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()");