diff --git a/CHANGELOG.md b/CHANGELOG.md index c1256f6..8814ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 TangleBrain-enforced guarantee, so a delegate that loses the env degrades safely to `unlinked` (never an error). This was the deferred half of the scatter-gather epic whose entry criterion was a live-verification spike — now done. +- **Knob panel surfaces the delegation tree.** The panel's "Delegated sub-tasks" card now shows a + **Linked to** stat (`N parent task(s)`, with any `unlinked` sub-calls noted) — GUI parity with the + `tanglebrain --stats` rollup, so the per-parent-task linkage is visible in the panel, not just the + CLI. Read-only; no new endpoint (the data already rides `view_stats`'s rollup payload). ### Fixed diff --git a/tanglebrain/gui/static/index.html b/tanglebrain/gui/static/index.html index ea6a977..53cd300 100644 --- a/tanglebrain/gui/static/index.html +++ b/tanglebrain/gui/static/index.html @@ -119,10 +119,18 @@

Pricing reference

const dg = s.delegates || {}; if (dg.count) { const backends = Object.entries(dg.by_backend || {}).map(([k, v]) => `${esc(k)} ${v.count || 0}`).join(", ") || "—"; + // Parent-task tree: how many top-level tasks the sub-calls link back to. Mirrors the CLI's + // "Linked to: N parent task(s)" line; sub-calls run outside a propagated task are "unlinked". + const byParent = dg.by_parent || {}; + const linked = Object.keys(byParent).filter((k) => k !== "unlinked").length; + const unlinked = (byParent.unlinked || {}).count || 0; + let tree = linked ? `${linked} parent task(s)` : `${unlinked} unlinked`; + if (linked && unlinked) tree += `, ${unlinked} unlinked`; html += `

Delegated sub-tasks (offloaded by orchestrators)

Sub-tasks
${dg.count}
By backend
${backends}
+
Linked to
${esc(tree)}
Est. tokens (in / out)
${(dg.in_tokens_est||0).toLocaleString()} / ${(dg.out_tokens_est||0).toLocaleString()}
Cloud-equiv
${money(dg.cloud_equiv_usd)}
diff --git a/tests/test_gui.py b/tests/test_gui.py index 4c8020a..c42efa9 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -124,6 +124,19 @@ def test_includes_delegate_breakdown(self): self.assertEqual(delegates["count"], 1) self.assertEqual(delegates["by_backend"]["local-x"]["count"], 1) + def test_includes_parent_task_tree(self): + # The panel's delegate card renders the by_parent tree, so view_stats must carry it through. + recs = [ + {"kind": "delegate", "model": "local-x", "parent_task_id": "p1"}, + {"kind": "delegate", "model": "local-x", "parent_task_id": "p2"}, + {"kind": "delegate", "model": "local-x"}, + ] + with patch("tanglebrain.gui.views.read_records", return_value=recs): + out = views.view_stats() + by_parent = out["summary"]["delegates"]["by_parent"] + self.assertEqual({k for k in by_parent if k != "unlinked"}, {"p1", "p2"}) + self.assertEqual(by_parent["unlinked"]["count"], 1) + class RunPromptTest(unittest.TestCase): def test_happy_path_reports_served(self):