feat(kanban): surface final_result for Done cards; show run summary when task.result is empty

This commit is contained in:
iborazzi 2026-06-08 08:38:20 +03:00 committed by Teknium
parent e4ea0a0ed7
commit deae8e3b4d
3 changed files with 115 additions and 4 deletions

View file

@ -3428,10 +3428,39 @@
onAddChild: props.onAddChild,
onRemoveChild: props.onRemoveChild,
}),
t.result ? h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head" }, tx(i18n, "result", "Result")),
h(MarkdownBlock, { source: t.result, enabled: props.renderMarkdown }),
) : null,
(function () {
var finalResult = t.final_result || t.result || t.latest_summary || null;
var isDone = t.status === "done";
var isParent = (t.link_counts && t.link_counts.children > 0);
if (finalResult) {
var label = t.result
? tx(i18n, "result", "Result")
: tx(i18n, "finalResult", "Final Result (run summary)");
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head" }, label),
h(MarkdownBlock, { source: finalResult, enabled: props.renderMarkdown }),
);
}
if (isDone && isParent) {
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head" }, tx(i18n, "result", "Result")),
h("div", { className: "hermes-kanban-done-no-result hermes-kanban-done-parent-note" },
tx(i18n, "doneParentNote",
"This card is an orchestrator / parent task. The substantive results are in the child cards listed in Dependencies below."),
),
);
}
if (isDone) {
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head" }, tx(i18n, "result", "Result")),
h("div", { className: "hermes-kanban-done-no-result" },
tx(i18n, "doneNoResult",
"No final result was recorded. Check Run History, Logs, or Child Tasks for the worker output."),
),
);
}
return null;
})(),
h(AttachmentsSection, {
attachments: attachments,
boardSlug: props.boardSlug,

View file

@ -172,6 +172,7 @@ def _task_dict(
# ``task_runs.summary`` (the kanban-worker pattern) instead of
# ``tasks.result``. ``None`` when no run has produced a summary yet.
d["latest_summary"] = latest_summary
d["final_result"] = task.result or latest_summary or None
# Keep body short on list endpoints; full body comes from /tasks/:id.
return d

View file

@ -2362,3 +2362,84 @@ def test_dashboard_failed_card_highlight_class_exists():
assert "hermes-kanban-card--failed" in js
assert "hermes-kanban-card--failed" in css
assert "failedIds" in js
# ---------------------------------------------------------------------------
# Final result visibility for Done cards
# ---------------------------------------------------------------------------
def test_task_dict_final_result_uses_tasks_result_first(client):
"""When tasks.result is set, final_result equals it (top priority)."""
r = client.post(
"/api/plugins/kanban/tasks",
json={"title": "Task with explicit result"},
)
task_id = r.json()["task"]["id"]
client.patch(
f"/api/plugins/kanban/tasks/{task_id}",
json={"status": "done", "result": "The final answer is 42.", "summary": "short handoff"},
)
r = client.get(f"/api/plugins/kanban/tasks/{task_id}")
assert r.status_code == 200
data = r.json()["task"]
assert data["result"] == "The final answer is 42."
assert data["final_result"] == "The final answer is 42."
def test_task_dict_final_result_falls_back_to_latest_summary(client):
"""When tasks.result is None but a run summary exists, final_result returns it."""
conn = kb.connect()
task_id = kb.create_task(conn, title="Task with only run summary")
kb.claim_task(conn, task_id)
kb.complete_task(conn, task_id, summary="Report written to /output/report.md")
conn.close()
r = client.get(f"/api/plugins/kanban/tasks/{task_id}")
assert r.status_code == 200
data = r.json()["task"]
assert data["status"] == "done"
assert not data["result"]
assert data["final_result"] == "Report written to /output/report.md"
def test_task_dict_final_result_none_when_nothing_recorded(client):
"""When neither tasks.result nor any run summary exists, final_result is None."""
r = client.post(
"/api/plugins/kanban/tasks",
json={"title": "Task with no result at all"},
)
task_id = r.json()["task"]["id"]
r = client.get(f"/api/plugins/kanban/tasks/{task_id}")
assert r.status_code == 200
assert r.json()["task"]["final_result"] is None
def test_board_tasks_include_final_result_field(client):
"""final_result must appear on board-level task cards too."""
conn = kb.connect()
task_id = kb.create_task(conn, title="Board card with summary only")
kb.claim_task(conn, task_id)
kb.complete_task(conn, task_id, summary="Done: see attachment")
conn.close()
r = client.get("/api/plugins/kanban/board")
assert r.status_code == 200
done_col = next(c for c in r.json()["columns"] if c["name"] == "done")
card = next((t for t in done_col["tasks"] if t["id"] == task_id), None)
assert card is not None
assert "Done: see attachment" in card["final_result"]
def test_dashboard_done_final_result_section_rendered_from_summary():
"""Frontend must render Final Result section from run summary when task.result is empty."""
repo_root = Path(__file__).resolve().parents[2]
dist = (repo_root / "plugins" / "kanban" / "dashboard" / "dist" / "index.js").read_text()
assert "final_result" in dist
assert "Final Result (run summary)" in dist
assert "No final result was recorded" in dist
assert "orchestrator" in dist or "parent task" in dist
# ---------------------------------------------------------------------------
# Final result visibility for Done cards
# ---------------------------------------------------------------------------