diff --git a/src/ui/public/app.js b/src/ui/public/app.js index 57abb4a..689eefb 100644 --- a/src/ui/public/app.js +++ b/src/ui/public/app.js @@ -117,6 +117,9 @@ const MANIFEST_EVENT_TRIGGERS = [ "branch_merged", ]; +const RUN_MANIFEST_EDITOR_VALUE = "__editor__"; +const RUN_MANIFEST_EDITOR_LABEL = "[Use Manifest Editor JSON]"; + function fmtMoney(value) { return `$${Number(value || 0).toFixed(4)}`; } @@ -838,6 +841,26 @@ function parseJsonSafe(text, fallback) { } } +function dedupeNonEmptyStrings(values) { + const output = []; + const seen = new Set(); + + for (const value of values) { + if (typeof value !== "string") { + continue; + } + + const normalized = value.trim(); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + output.push(normalized); + } + + return output; +} + function populateSelect(select, values, selectedValue) { const previous = selectedValue || select.value; select.innerHTML = ""; @@ -857,6 +880,35 @@ function populateSelect(select, values, selectedValue) { } } +function populateRunManifestSelect(values, selectedValue) { + const previous = selectedValue || dom.runManifestSelect.value; + dom.runManifestSelect.innerHTML = ""; + + const editorOption = document.createElement("option"); + editorOption.value = RUN_MANIFEST_EDITOR_VALUE; + editorOption.textContent = RUN_MANIFEST_EDITOR_LABEL; + dom.runManifestSelect.append(editorOption); + + for (const value of values) { + const option = document.createElement("option"); + option.value = value; + option.textContent = value; + dom.runManifestSelect.append(option); + } + + if (previous && [...dom.runManifestSelect.options].some((option) => option.value === previous)) { + dom.runManifestSelect.value = previous; + return; + } + + if (state.selectedManifestPath && values.includes(state.selectedManifestPath)) { + dom.runManifestSelect.value = state.selectedManifestPath; + return; + } + + dom.runManifestSelect.value = RUN_MANIFEST_EDITOR_VALUE; +} + async function loadConfig() { const payload = await apiRequest("/api/config"); state.config = payload.config; @@ -888,14 +940,19 @@ async function loadConfig() { async function loadManifests() { const payload = await apiRequest("/api/manifests"); - state.manifests = payload.manifests; + const manifests = dedupeNonEmptyStrings([ + ...(Array.isArray(payload.manifests) ? payload.manifests : []), + state.selectedManifestPath, + dom.manifestPath.value, + ]); + state.manifests = manifests; if (!state.selectedManifestPath && state.manifests.length > 0) { state.selectedManifestPath = state.manifests[0]; } populateSelect(dom.graphManifestSelect, state.manifests, state.selectedManifestPath); - populateSelect(dom.runManifestSelect, state.manifests, state.selectedManifestPath); + populateRunManifestSelect(state.manifests, state.selectedManifestPath); if (state.selectedManifestPath) { dom.manifestPath.value = state.selectedManifestPath; @@ -1332,9 +1389,10 @@ async function startRun(event) { return; } + const manifestSelection = dom.runManifestSelect.value.trim(); + const payload = { prompt, - manifestPath: dom.runManifestSelect.value, executionMode: dom.runExecutionMode.value, provider: dom.runProvider.value, topologyHint: dom.runTopologyHint.value.trim() || undefined, @@ -1342,6 +1400,20 @@ async function startRun(event) { simulateValidationNodeIds: fromCsv(dom.runValidationNodes.value), }; + if (manifestSelection === RUN_MANIFEST_EDITOR_VALUE) { + const manifestFromEditor = parseJsonSafe(dom.manifestEditor.value, null); + if (!manifestFromEditor) { + showRunStatus("Manifest editor JSON is invalid. Save or fix it before running.", true); + return; + } + payload.manifest = manifestFromEditor; + } else if (manifestSelection) { + payload.manifestPath = manifestSelection; + } else { + showRunStatus("Select a manifest path or choose editor JSON.", true); + return; + } + try { const response = await apiRequest("/api/runs", { method: "POST", @@ -1458,7 +1530,7 @@ async function loadManifestEditor() { dom.manifestPath.value = payload.manifest.path; state.selectedManifestPath = payload.manifest.path; populateSelect(dom.graphManifestSelect, state.manifests, state.selectedManifestPath); - populateSelect(dom.runManifestSelect, state.manifests, state.selectedManifestPath); + populateRunManifestSelect(state.manifests, state.selectedManifestPath); setManifestStatus(`Loaded ${payload.manifest.path}.`); } catch (error) { setManifestStatus(error instanceof Error ? error.message : String(error), true); @@ -1511,7 +1583,9 @@ async function saveManifest(event) { renderManifestHelper(); await loadManifests(); dom.graphManifestSelect.value = state.selectedManifestPath; - dom.runManifestSelect.value = state.selectedManifestPath; + if ([...dom.runManifestSelect.options].some((option) => option.value === state.selectedManifestPath)) { + dom.runManifestSelect.value = state.selectedManifestPath; + } setManifestStatus(`Saved ${payload.manifest.path}.`); await refreshGraph(); } catch (error) { @@ -1528,7 +1602,9 @@ function bindUiEvents() { dom.graphManifestSelect.addEventListener("change", async () => { state.selectedManifestPath = dom.graphManifestSelect.value; - dom.runManifestSelect.value = state.selectedManifestPath; + if ([...dom.runManifestSelect.options].some((option) => option.value === state.selectedManifestPath)) { + dom.runManifestSelect.value = state.selectedManifestPath; + } dom.manifestPath.value = state.selectedManifestPath; await refreshGraph(); });