diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index f92c6c0..83fb17d 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -53,6 +53,7 @@ const appState = { onelinerSessions: [], selectedOnelinerSessionId: "", onelinerRuns: [], + onelinerRunFilter: "focus", selectedOnelinerRunId: "", lastCompletedOnelinerRunId: "", onelinerMessages: [], @@ -984,6 +985,14 @@ function renderOneLinerRunsHtml() { const pendingRunCount = safeArray(runs).filter((item) => item.run_status === "needs_confirmation").length; const activeRunCount = safeArray(runs).filter((item) => ["queued", "running", "blocked"].includes(item.run_status)).length; const completedRunCount = safeArray(runs).filter((item) => item.run_status === "done").length; + const filterKey = String(appState.onelinerRunFilter || "").trim() || (activeRunCount || pendingRunCount ? "focus" : "done"); + const runFilterPredicates = { + focus: (item) => ["needs_confirmation", "queued", "running", "blocked"].includes(item.run_status), + done: (item) => item.run_status === "done", + all: () => true + }; + const filteredRuns = safeArray(runs).filter(runFilterPredicates[filterKey] || runFilterPredicates.focus); + const visibleRuns = (filteredRuns.length ? filteredRuns : runs).slice(0, 8); const recentCompletedRuns = safeArray(runs) .filter((item) => item.run_status === "done" && item.id !== currentRun.id) .slice(0, 3); @@ -1027,9 +1036,14 @@ function renderOneLinerRunsHtml() { 执行中 ${escapeHtml(formatNumber(activeRunCount))} 已完成 ${escapeHtml(formatNumber(completedRunCount))} +
+ 重点运行 + 已完成 + 全部 +
${runs.length > 1 ? `
- ${runs.slice(0, 6).map((item) => ` + ${visibleRuns.map((item) => ` ${escapeHtml(brief(item.title || item.plan?.goal || "主 Agent 任务", 14))} · ${escapeHtml({ needs_confirmation: "待确认", @@ -2118,6 +2132,8 @@ function openCurrentOneLinerRunResultAction(runId = "") { renderAll(); return; } + appState.selectedOnelinerRunId = currentRun.id; + appState.onelinerRunFilter = currentRun.run_status === "done" ? "done" : appState.onelinerRunFilter; openActionModal({ title: currentRun.title || currentRun.plan?.goal || "主 Agent 执行结果", description: currentRun.result?.execution_summary || currentRun.status_summary || "这是当前主 Agent 任务的执行结果。", @@ -10002,6 +10018,11 @@ document.addEventListener("click", async (event) => { renderAll(); return; } + if (name === "select-oneliner-run-filter") { + appState.onelinerRunFilter = action.dataset.runFilter || "focus"; + renderAll(); + return; + } if (name === "select-oneliner-session") { appState.selectedOnelinerSessionId = action.dataset.sessionId || ""; await loadOneLinerMessages(appState.selectedOnelinerSessionId); diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 40b320c..abcc707 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -313,9 +313,19 @@ test("oneliner runtime shows grouped run health summary above the current run ca assert.match(runtime, /已完成/); assert.match(runtime, /最近完成/); assert.match(runtime, /recentCompletedRuns/); + assert.match(runtime, /select-oneliner-run-filter/); + assert.match(runtime, /重点运行/); + assert.match(runtime, /已完成/); + assert.match(runtime, /全部/); assert.match(runtime, /safeArray\(runs\)\.filter\(\(item\) => item\.run_status === "needs_confirmation"\)\.length/); }); +test("opening a main agent run result keeps that run selected in the floating runtime", () => { + const resultAction = extractBetween(APP, "function openCurrentOneLinerRunResultAction(runId = \"\")", "function openConfirmOneLinerRunAction(runId = \"\")"); + assert.match(resultAction, /appState\.selectedOnelinerRunId = currentRun\.id/); + assert.match(resultAction, /appState\.onelinerRunFilter = currentRun\.run_status === "done" \? "done" : appState\.onelinerRunFilter/); +}); + test("oneliner panel auto-polls active runs while the floating panel stays open", () => { const render = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()"); const open = extractBetween(APP, "function openOneLinerPanel()", "function closeOneLinerPanel()");