From fa437343335a75d8602a940fa34f2c916cc6872f Mon Sep 17 00:00:00 2001 From: Jason-Vaughan <95194903+Jason-Vaughan@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:56:53 -0700 Subject: [PATCH] Surface the delegation tree in the knob panel (GUI parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-parent-task tree shipped in --stats (CLI) but the panel's "Delegated sub-tasks" card ignored by_parent. Add a "Linked to" stat (N parent task(s), with any unlinked sub-calls noted) mirroring the CLI rollup. Read-only, no new endpoint — the data already rides view_stats's rollup payload. --- CHANGELOG.md | 4 ++++ tanglebrain/gui/static/index.html | 8 ++++++++ tests/test_gui.py | 13 +++++++++++++ 3 files changed, 25 insertions(+) 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):