Skip to content

feat(web): nest delegated threads under their parent in the sidebar - #587

Merged
rynfar merged 2 commits into
pylonfrom
feat/sidebar-delegated-children
Sep 17, 2026
Merged

rynfar merged 2 commits into
pylonfrom
feat/sidebar-delegated-children

Conversation

@rynfar

@rynfar rynfar commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Delegated child threads showed up as unrelated top-level rows in the sidebar. They were often sorted above their parent, so it was hard to tell which agent started which task.

What changed

  • Shared helper (@t3tools/client-runtime/state/delegated-threads):
    • Reads the parent from the child id format delegated:<parent>:<16 hex>. Parent ids can contain colons, so it doesn't split on :.
    • Nests each child under a parent in the same list.
    • Keeps a child top-level when its parent isn't in that list, so a thread never disappears.
  • Live sidebar (Sidebar.tsx):
    • Grouping runs per section (pinned, active, snoozed, settled). Children render indented under their parent.
    • Pin, snooze and settle are per thread, so when a child and its parent end up in different sections, the child shows as its own row there.
    • Paging ("Show more"), collapsed shelves and drag order use only top-level rows. A child's own order keys are never shown, so a drop never writes them.
    • Jump shortcuts, shift-range select and next/previous thread follow the rendered order, children included.
    • The open thread stays visible when its parent sits behind a collapsed shelf or "Show more".
    • A parent row with children wraps them in its sortable node, so the group moves as one during a drag.
    • The drag listeners and the file-drop target stay on the parent's own row, so pressing or dropping files on a child never acts on the parent.
    • Rows without children render exactly as before.
  • Docs: docs/user/agent-delegation.md explains where children appear.

Not covered: the legacy per-project sidebar and the mobile app still list children as ordinary threads. The helper lives in client-runtime so mobile can adopt it later. There is no collapse toggle per parent (a parent has at most eight live children).

Before / after

Isolated dev server with a fixture: a parent with two delegated children, an unrelated thread, and a child whose parent is settled.

Before After
Sidebar before: children listed above their parent Sidebar after: children indented under their parent

"Audit lockfile changes" stays top-level in both because its parent is settled.

Verification

  • Focused tests:
    • vp test run packages/client-runtime/src/state/delegatedThreads.test.ts: 8 new tests covering id parsing, colon-containing parent ids, malformed ids, environment scoping, orphan fallback, flattening and route containment.
    • Sidebar.logic, Sidebar.drag, Sidebar.motion and Sidebar.pointer tests: 259 passing together with the new file.
  • Static checks:
    • vp run -F web typecheck and vp run -F @t3tools/client-runtime typecheck pass, re-run on web after rebasing onto pylon.
    • vp run knip:check passes.
    • Lint shows no new warnings: 24 before and after on Sidebar.tsx.
  • Browser, on the isolated dev server:
    • Children render nested.
    • Clicking a child opens the child.
    • Pressing and dragging on a child does not start a drag.
    • Dragging the parent translates the whole group.
    • Dropping the parent into Settled settled it on the server, and its children became top-level active rows.
    • Un-settling it nested them again.

Adversarial review (Fable)

The first pass found no blocking issues but verified three real defects, all fixed in c128af68c1:

  1. Wrong drag gap. The gap was measured from a group's height. The layout code now takes groupKeys: group rows no longer set the base row height, and a lifted group keeps its own height. Two new tests fail on the old code.
  2. Optimistic hold released early. This happened when a parent was dropped into the section holding its children. resolveSidebarDropTarget now takes the dragged row's children as nestingKeys and removes them from the destination order, so their order keys are never written. Live check: I pinned the parent, then dragged it back into Active. The order stayed stable across 4 seconds of samples, the children re-nested, and their order keys stayed null in SQLite.
  3. Settled parent hidden during a drag. This happened when its nested child was the open thread. The route exception now receives the top-level row that holds the open thread.

Also fixed: a grandchild stays top-level (one-level guard, with a test), and an attribute the live sidebar never reads was removed from the nested list.

The re-review of the fixes found no blocking issues and nothing new. One accepted preview inaccuracy: when a group moves between Pinned and Active, the gap is group-sized, and the children split back out on release. Parent rows with children still re-render on each shell update, because the nested lists are rebuilt on every update anyway.

Done by Claude Opus 5 in Claude Code.

@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pylon-marketing Ready Ready Preview Sep 17, 2026 5:54am UTC

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.9 KiB 13.9 KiB −13 B (−0.1%) 15.1 KiB ✅
Codex Thread snapshot wire 7.2 KiB 7.2 KiB +2 B (+0.0%) 7.3 KiB ✅
Codex Live turn WebSocket wire 6.7 KiB 6.7 KiB −15 B (−0.2%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 58.0 KiB 58.0 KiB 0 B (0.0%) 66.4 KiB ✅
Codex Live turn messages 8 8 0 (0.0%) 21 ✅
Claude Total thread wire 14.1 KiB 14.1 KiB +15 B (+0.1%) 15.1 KiB ✅
Claude Thread snapshot wire 7.2 KiB 7.2 KiB −12 B (−0.2%) 7.3 KiB ✅
Claude Live turn WebSocket wire 6.8 KiB 6.9 KiB +27 B (+0.4%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 59.7 KiB 59.7 KiB +44 B (+0.1%) 66.4 KiB ✅
Claude Live turn messages 9 10 +1 (+11.1%) 21 ✅

Baseline: 00b2ffc · PR result: c128af6 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 115.7 KiB
  • Claude decoded thread snapshot: 116.3 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar
rynfar merged commit e940e1c into pylon Sep 17, 2026
20 checks passed
@rynfar
rynfar deleted the feat/sidebar-delegated-children branch September 17, 2026 08:02

This branch was successfully deployed

1 active deployment
Preview — c128af68 Deployed Sep 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant