Skip to content

feat(sidebar): reveal nested subagent threads#3861

Open
PixPMusic wants to merge 10 commits into
pingdotgg:t3code/codex-turn-mappingfrom
PixPMusic:pixpmusic/subagent-sidebar-threads
Open

feat(sidebar): reveal nested subagent threads#3861
PixPMusic wants to merge 10 commits into
pingdotgg:t3code/codex-turn-mappingfrom
PixPMusic:pixpmusic/subagent-sidebar-threads

Conversation

@PixPMusic

@PixPMusic PixPMusic commented Jul 10, 2026

Copy link
Copy Markdown

Summary

  • add an opt-in sidebar setting that nests subagent threads beneath their parent
  • persist per-parent expansion state and keep orphaned or cyclic relationships safe
  • share the thread-tree projection through client runtime for reuse across clients

Validation

  • vp check
  • vp run typecheck
  • vp test apps/web/src/components/Sidebar.logic.test.ts packages/client-runtime/src/state/threadRelationships.test.ts packages/contracts/src/settings.test.ts apps/desktop/src/settings/DesktopClientSettings.test.ts

Stacked on #2829.


Note

High Risk
Large orchestration changes to thread lifecycle, event commit guards, and checkpoint deletion affect core concurrency and data cleanup; incorrect guards could drop events or over-delete refs.

Overview
Orchestration v2 now treats archive, unarchive, and delete as owned-subagent subtree operations: the orchestrator walks ownedSubagentDescendants, applies lifecycle mutations with coordinated timestamps, cancels in-flight runs/attempts/nodes/subagents/provider turns, detaches sessions, and enqueues terminal, attachment, and checkpoint.cleanup effects on delete. Commands run under a root-graph lock (lineage rootThreadId) alongside per-thread locks; archive/delete cancel process-bound effects across all affected thread IDs. Startup and live thread.created repair (planOwnedSubagentLifecycleRepairs) fixes parent/child lifecycle mismatches. Unarchive blocks when the parent is archived/deleted; archiving a subagent cancels the parent’s active subagent task artifacts.

EventSink writeIfRunCurrent is stricter: target threads must be active (not archived/deleted), expectedStatus can be an array, and allowSupersededAttemptTerminalArtifacts admits late terminal provider-turn / interrupt-result writes from superseded attempts. Routed events into archived child threads are rejected. Provider turn start and run execution detach sessions and gate writes when threads go inactive mid-flight.

New CheckpointCleanupService and deleteScopeRefs (through latest run/checkpoint ordinal) remove checkpoint refs when threads are deleted. getThreadLifecycleRecords keeps deleted thread headers for repair.

Web sidebar logic adds nested subagent tree helpers (flatten, roots, ancestors, expansion merge for deep links, range selection order) wired through client-runtime thread-relationships. Desktop settings tests add default sidebarShowSubagentThreads: false.

Reviewed by Cursor Bugbot for commit c050552. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reveal nested subagent threads in the sidebar with expand/collapse controls

  • Adds a collapsible nested subagent thread tree to the sidebar, with per-row expand/collapse chevrons, depth indentation, active descendant counts, and a 'Show nested subagents' toggle in the sort menu.
  • Persists expanded branch keys to uiStateStore and auto-expands ancestors of the currently active thread on first paint.
  • Archive, unarchive, and delete operations now cascade over the entire owned-subagent subtree with confirmation dialogs that show descendant and active-work counts; delete also stops sessions and clears terminal/UI state for every subtree member.
  • Introduces CheckpointCleanupServiceV2 that deletes checkpoint refs per scope up to the latest known ordinal, wired into the effect worker as a new checkpoint.cleanup effect type.
  • The orchestrator serializes archive/unarchive/delete dispatch per root thread via a graph-level lock, rejects create/fork when the parent is archived or deleted, and repairs inconsistent owned-subagent lifecycle states on startup.
  • EventSinkV2.write now rejects writes targeting archived/deleted threads and accepts multiple expected run statuses; provider adapter events are only persisted when the root run is current.
  • Risk: archiving no longer hard-blocks on running status at the useThreadActions layer; cascade deletes affect all owned subagent descendants, not just the selected thread.

Macroscope summarized c050552.


Archiving, unarchiving, or deleting a thread now applies to the subagent subtree it recursively owns, enforced server-side in this PR: the orchestrator walks the owned subtree (OwnedSubagentTree), applies the lifecycle transition to every member with a single timestamp, and cancels active runs and pending attempts. Half-applied cascades are repaired on startup (OwnedSubagentLifecycleRepair) and checkpoints orphaned by deleted threads are cleaned up (CheckpointCleanupService). The web sidebar layers on top: subtree-aware confirmation copy and counts, recovery-root badges, navigation, and client-side draft/terminal cleanup. No mobile code in this PR — the mobile UI for the same behavior follows in #3871.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6ccd4b0a-5964-4ab9-bde0-6b9f0cdaa263

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 10, 2026
Comment thread packages/client-runtime/src/state/threadRelationships.ts
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

3 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

: rootProjectThreads.filter((thread) =>
visibleRootThreadKeys.has(sidebarThreadKey(thread)),
);
return flattenSidebarSubagentTree({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/Sidebar.tsx:3693

The sidebar thread preview limit (sidebarThreadPreviewCount) is applied to root threads only, not to the final flattened row list, so a project can render far more visible rows than the configured limit. When subagent branches are expanded, flattenSidebarSubagentTree expands each previewed root into all of its visible descendants, so a project configured to show 3 visible threads can render 3 roots plus every expanded descendant. This breaks the "Visible threads" setting and skews downstream features like jump-hint numbering and sidebar prewarming, which now treat those extra rows as visible. The preview slicing should be applied after flattening (or the flatten result should be truncated to sidebarThreadPreviewCount) so the total visible row count respects the configured limit.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/Sidebar.tsx around line 3693:

The sidebar thread preview limit (`sidebarThreadPreviewCount`) is applied to root threads only, not to the final flattened row list, so a project can render far more visible rows than the configured limit. When subagent branches are expanded, `flattenSidebarSubagentTree` expands each previewed root into all of its visible descendants, so a project configured to show 3 visible threads can render 3 roots plus every expanded descendant. This breaks the "Visible threads" setting and skews downstream features like jump-hint numbering and sidebar prewarming, which now treat those extra rows as visible. The preview slicing should be applied after flattening (or the flatten result should be truncated to `sidebarThreadPreviewCount`) so the total visible row count respects the configured limit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional: the "Visible threads" setting controls how many conversations (root threads) are previewed per project, and subagent children only render when the user explicitly expands a branch — so the extra rows are opt-in rather than a regression of the limit. Truncating the flattened list instead would cut a tree mid-branch, leaving a parent marked expanded with some of its children silently missing. Prewarming and jump hints are independently capped, so expanded rows don't create unbounded work downstream.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/web/src/components/Sidebar.tsx
@PixPMusic

Copy link
Copy Markdown
Author

Note on the failing Macroscope - Effect Service Conventions check: its own agent log concludes this PR is frontend React code and pure helpers with no Effect-service changes to flag, but the run then fails on an output-format conflict inside the check-run agent (JSON schema vs a literal "All clear" reply — the activity log cuts off mid-sentence). The failure is a check-tooling issue on this repo's .macroscope/check-run-agents/effect-service-conventions.md, not a convention violation in this diff. The repository Check job and all other checks are green.

PixPMusic added a commit to PixPMusic/t3code that referenced this pull request Jul 10, 2026
Extract the environment-scoped subagent tree projection with cycle and
orphan fallbacks so every client renders the same rows. The web sidebar
adopts it separately in pingdotgg#3861; this copy carries only the shared
helpers the mobile tree needs.
Web half of the owned-subtree lifecycle work: sidebar thread actions
archive, unarchive, and delete a root together with its recursively
owned subagent threads, using the shared subtree helpers and
confirmation copy from client-runtime, with an app setting to skip the
archive confirmation. The client-runtime helpers are byte-identical
with the mobile-subagent-tree branch so the shared file merges cleanly
in either order.
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 10, 2026
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/hooks/useThreadActions.ts
scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
? rootProjectThreads
: rootProjectThreads.slice(0, sidebarThreadPreviewCount);
const visibleRootThreadKeys = new Set(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/Sidebar.tsx:3804

When the current route points at a nested subagent thread inside a collapsed project, pinnedCollapsedThread walks up to the topmost ancestor and only that root key is added to visibleSidebarThreadKeys. The active child thread's own key is missing from the list, so resolveAdjacentThreadId cannot find currentThreadId and thread.previous / thread.next return null from that route. Consider including the active thread key itself (and its ancestor chain) in the visible set when a pinned-collapsed thread is used.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/Sidebar.tsx around line 3804:

When the current route points at a nested subagent thread inside a collapsed project, `pinnedCollapsedThread` walks up to the topmost ancestor and only that root key is added to `visibleSidebarThreadKeys`. The active child thread's own key is missing from the list, so `resolveAdjacentThreadId` cannot find `currentThreadId` and `thread.previous` / `thread.next` return `null` from that route. Consider including the active thread key itself (and its ancestor chain) in the visible set when a pinned-collapsed thread is used.

Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
Review follow-ups: deleting a root now stops sessions and closes
terminals for every thread in the owned subtree rather than only the
root; the environment-thread fallback read merges loaded archived
shells so subtree confirmations are not undercounted before the
snapshot arrives; and sidebar descendant counts and recovery-root
badges are computed over unarchived threads only, matching the archive
confirmation flow.
const archived = readArchivedThreadShells(environmentId)
.filter((thread) => !activeIds.has(thread.id))
.map((thread) => presentThreadShell(environmentId, thread));
return [...active, ...archived];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale archived cache skews subtree

Medium Severity

When the environment shell snapshot is unavailable, readEnvironmentThreads merges archived threads only from readArchivedThreadShells, which returns an empty list until that archived snapshot atom has already succeeded. Subtree reads for delete/archive confirmations and the new per-subtree stopThreadSession / closeTerminal loops can then omit archived owned subagents, so dialogs undercount and client teardown skips them even if the server still removes the tree.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 83f95f5. Configure here.

renderedThreads.map((row) => {
const { thread } = row;
const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
const subagentDescendants = getOwnedSubagentDescendants(unarchivedProjectThreads, thread);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/Sidebar.tsx:1155

getOwnedSubagentDescendants is called with unarchivedProjectThreads, which is scoped to the current sidebar project. Subagent lineage is environment-wide, so a parent thread can own descendants in other projects. This causes subagentDescendantCount to undercount the subtree, but SidebarThreadRow still calls attemptArchiveThread(threadRef, { confirmed: true }) with that count. The Confirm +N UI therefore discloses fewer threads than the archive operation actually affects, so the user can archive more descendant threads than the confirmation message showed. Consider passing the full set of environment-wide thread shells to getOwnedSubagentDescendants so the count matches the archive scope.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/Sidebar.tsx around line 1155:

`getOwnedSubagentDescendants` is called with `unarchivedProjectThreads`, which is scoped to the current sidebar project. Subagent lineage is environment-wide, so a parent thread can own descendants in other projects. This causes `subagentDescendantCount` to undercount the subtree, but `SidebarThreadRow` still calls `attemptArchiveThread(threadRef, { confirmed: true })` with that count. The `Confirm +N` UI therefore discloses fewer threads than the archive operation actually affects, so the user can archive more descendant threads than the confirmation message showed. Consider passing the full set of environment-wide thread shells to `getOwnedSubagentDescendants` so the count matches the archive scope.

Archive, delete, and unarchive now dispatch a command for every thread
in the owned subagent subtree instead of only the selected root, so
the recursive behavior promised by the confirmation copy holds without
any server-side changes. Unarchive matches descendants archived
together with the root using a small timestamp tolerance, since each
member is archived by its own command.
Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
The recursive lifecycle for owned subagent subtrees is enforced
server-side in this PR: one command per root, one timestamp per
cascade, run cancellation included. Archive, delete, and unarchive go
back to a single root dispatch, and "archived with it" matching
returns to exact timestamp equality, which the server-side cascade
guarantees.
Applying archive, unarchive, or delete to a thread now cascades
through the subagent threads it recursively owns: the orchestrator
walks the owned subtree (OwnedSubagentTree), applies the lifecycle
transition to every member with a single timestamp, and cancels their
active runs and pending attempts. Half-applied cascades are repaired
on startup (OwnedSubagentLifecycleRepair), and checkpoints orphaned by
deleted threads are cleaned up (CheckpointCleanupService).
@PixPMusic PixPMusic force-pushed the pixpmusic/subagent-sidebar-threads branch from 50628fd to c050552 Compare July 11, 2026 02:31
PixPMusic added a commit to PixPMusic/t3code that referenced this pull request Jul 11, 2026
Mobile UI for the owned-subtree lifecycle cascade in pingdotgg#3861: thread
list actions archive, unarchive, and delete a root together with its
recursively owned subagent threads using the shared subtree helpers
and confirmation copy from client-runtime, and thread lists flag
recovery roots whose subagent parent is no longer available. Depends
on the server-side cascade in pingdotgg#3861.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c050552. Configure here.

const descendants = ownedSubagentDescendants(command.threadId, [
...shell.threads,
...shell.archivedThreads,
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root cascade skips orphan subagents

Medium Severity

dispatchOwnedThreadLifecycle builds the owned subtree only from active and archived shell threads via ownedSubagentDescendants. That BFS starts at the command thread and follows subagent parent links present in that list. A descendant whose direct parent was already deleted is not linked from the root, so it is omitted from archive, delete, and related cleanup even when its lineage.rootThreadId still points at the root being mutated.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c050552. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant