Skip to content

🤖 feat: sub-agent tasks strip in the VS Code webview dock - #5657

Merged
ThomasK33 merged 1 commit into
mainfrom
xum-5109-webview-subagent-dock
Oct 4, 2026
Merged

ThomasK33 merged 1 commit into
mainfrom
xum-5109-webview-subagent-dock

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Summary

Last layer of the #5109 stack, after #5651 (merged: live host workspace list). The VS Code webview dock now shows the desktop sub-agent tasks strip above the background processes strip. It reuses the shared SubAgentTasksContent (#5634). A new SubAgentTasksDock feeds it from the host's live workspace list and a per-workspace activity map. Clicking a row asks the host to select that sub-agent. chatUiCapabilities.subAgentTasks is now supported.

This layer also moves the host's activity pump from "selected workspace, tied to the chat stream" to "selected workspace and its descendants, with its own controller". The activity set is recomputed only when membership changes.

Refs #5109. Nested-run discovery is deferred to #5652; see Scope. I will close #5109 by hand after this PR merges, with a residual comment.

Implementation

Host (extension.ts, workspaceActivityPump.ts):

  • The activity pump runs for [selected, ...descendants] and posts a per-workspace map { activeBashMonitorCount, streaming, activeWorkflowRunIds }. Unchanged maps are not posted. Goal-only (transientGoalOnly) events are skipped because they carry stale baseline fields.
  • The selected + descendants set is computed in one pass over the list (a parent → children map). The host recomputes it only on a selection change, the snapshot, or a metadata update that changes a workspace's parent link (task creation or removal included). It restarts the pump (one bulk activity.list) only when the set differs. Activity events never recompute it: the pump's tracked set is fixed when it starts.
  • The pump has its own controller, so it no longer stops with the chat stream. It stops on deselection, file mode, refresh failure or dispose. isSelected also ignores callbacks from a replaced pump.
  • The host's UiWorkspace gains a narrow task slice, for sub-agent workspaces only: title, taskStatus, taskExecutionStatus, workflowTask, reportedAt, archivedAt. No paths or prompts. The per-workspace projection compare from 🤖 feat: keep the VS Code host's workspace list live #5651 includes it, so a task status change re-posts the list and an unrelated event still posts nothing.

Webview and shared code:

  • The webview takes the selected workspace's monitor count from the activity map, so the waiting-on-monitor barrier works as before.
  • The tray helpers take a SubAgentTaskWorkspace (Pick<FrontendWorkspaceMetadata, …>) instead of the full metadata. isWorkspaceDelegatedActivityActive takes the four fields it reads. Desktop callers pass full metadata unchanged, and the webview builds the Pick from UiWorkspace without fabricating fields.
  • SubAgentTasksDock runs the same collectDescendantAgents and mergeActiveWorkflowGroups as desktop. Its per-descendant hints come from activity: monitors, plus a "working" hint = streaming, active workflow runs or armed monitors. The expanded state uses the desktop's persisted key.
  • No new IPC message type, no new persisted field, no new bridge procedure, no manual memo.

Scope: nested-run discovery deferred to #5652

Desktop also calls workflows.listActiveRuns (cold-mount discovery) and polls workflows.getRunStatuses for nested (workflow-in-workflow) runs between workers. Those procedures are not bridged. Without them, a nested run's header leaves the strip once its workers finish. Its enclosing top-level run stays visible from activity as "Workflow · running" (tested), so the strip never reads as empty or "all finished" while a workflow runs. #5652 tracks bridging both procedures.

Activity set measurement (perf-owner requirement 3)

In-process harness (the same one as #5651: real XumChatViewProvider, local oRPC server, 4,502 workspaces), with ws-1 selected and one sub-agent task under it. A timing-free counter patch counts calls of the set computation. Measured before the two #5651 review fixes were rebased in; they do not touch the activity path.

Measure Result
Metadata events in 60 s (10 per second) 592: 533 on unrelated workspaces, 59 renames of the selected task
Selected + descendants recomputes during them 0
Activity pump restarts during them 0
workspaces posts during them 59 (one per task rename, 0 for the unrelated events)
A new task appears under the selection 1 recompute, 1 restart, 27.83 ms to the new activity post, which tracks ws-1, ws-child, ws-child-2
Harness (kept outside the repo)

Built with the make-bench.sh from #5651, then run from vscode/ as bun test src/bench5109.test.ts -t bench5109b.

Counter patch applied to extension.ts for the run:

diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts
index d5a74af34e..191328dd56 100644
--- a/vscode/src/extension.ts
+++ b/vscode/src/extension.ts
@@ -160,6 +160,7 @@ function toUiWorkspace(workspace: WorkspaceWithContext): UiWorkspace {
 
 /** The workspace and its descendants, parents before children; one pass over the list. */
 function selfAndDescendantIds(workspaces: readonly WorkspaceWithContext[], rootId: string): string[] {
+  (globalThis as any).__recomputes = ((globalThis as any).__recomputes ?? 0) + 1;
   const children = new Map<string, string[]>();
   for (const { id, parentWorkspaceId } of workspaces) {
     if (parentWorkspaceId == null) continue;

Benchmark appended to the copy of extension.test.ts:

// ---- #5109 PR2b benchmark (scratch, not committed): activity set recomputes under load ----
describe("bench5109b", () => {
  test("bench5109b: 10 unrelated metadata events/s for 60 s, selected workspace with a task", async () => {
    const SECONDS = Number(process.env.BENCH_SECONDS ?? 60);
    const synthetic = Array.from({ length: 4_500 }, (_, i) => ({
      ...WORKSPACE,
      id: `ws-s${i}`,
      name: `s${i}`,
      createdAt: "2026-09-01T00:00:00.000Z",
    }));
    const child = { ...WORKSPACE, id: "ws-child", name: "child", parentWorkspaceId: WORKSPACE.id };
    const harness = await setup([WORKSPACE, child, ...synthetic]);
    const { state } = harness.server;
    type Msg = PostedMessage & { workspaces?: unknown[]; activity?: Record<string, unknown> };
    const posted = harness.posted as Msg[];
    const count = (type: string) => posted.filter((m) => m.type === type).length;
    harness.send({ type: "selectWorkspace", workspaceId: WORKSPACE.id });
    await until(() => count("workspaceActivity") >= 1, "the first activity post");
    await new Promise((r) => setTimeout(r, 1000));

    const lists0 = state.activityLists;
    const recomputes0 = (globalThis as any).__recomputes ?? 0;
    const posts0 = count("workspaces");
    let sent = 0;
    const emitter = setInterval(() => {
      // Unrelated to the activity set: synthetic workspaces, plus a rename of the selected task.
      const target = sent % 10 === 9 ? { ...child, title: `T${sent}` } : synthetic[(sent * 37) % synthetic.length];
      state.metadata.push({ workspaceId: target.id, metadata: { ...target, pinned: sent % 2 === 0 } });
      sent += 1;
      notify();
    }, 100);
    await new Promise((r) => setTimeout(r, SECONDS * 1000));
    clearInterval(emitter);
    await new Promise((r) => setTimeout(r, 500));
    const restartsUnderLoad = state.activityLists - lists0;
    const recomputesUnderLoad = ((globalThis as any).__recomputes ?? 0) - recomputes0;
    const listPosts = count("workspaces") - posts0;

    // A membership change: a second task appears under the selection.
    const t0 = performance.now();
    const activityPosts = count("workspaceActivity");
    state.metadata.push({ workspaceId: "ws-child-2", metadata: { ...child, id: "ws-child-2", name: "child-2" } });
    notify();
    await until(() => count("workspaceActivity") > activityPosts, "the restarted pump's post");
    const restartMs = performance.now() - t0;
    const last = posted.filter((m) => m.type === "workspaceActivity").at(-1);

    console.log(
      [
        "BENCH5109B",
        `workspaces listed: ${synthetic.length + 2}; window: ${SECONDS} s; selected ws-1 with 1 task`,
        `load: ${sent} metadata events (${Math.floor(sent / 10)} task renames, the rest unrelated); selected + descendants recomputes ${recomputesUnderLoad}; activity pump restarts ${restartsUnderLoad}; workspaces posts ${listPosts}`,
        `membership change (new task): recomputes ${((globalThis as any).__recomputes ?? 0) - recomputes0 - recomputesUnderLoad}; restarts ${state.activityLists - lists0 - restartsUnderLoad}; latency to the new activity post ${restartMs.toFixed(2)} ms; tracked ${Object.keys(last?.activity ?? {}).join(",")}`,
      ].join("\n")
    );
  }, 10 * 60_000);
});

Tests

  • Host lifecycle (extension.test.ts):
    • Switching workspaces: selecting none and then a workspace starts a new pump (a new activity.list).
    • Removing a descendant restarts the pump for the smaller set, and the next post no longer contains the child. An unrelated update and a rename of the child (a list re-post) keep the running pump.
    • Late events from a replaced pump: the first pump's activity.list is held while removing the descendant replaces the pump. When the held read answers, it posts nothing; only the replacement's map is posted. With the abort and the isSelected controller check both removed, this case fails (12 extra lines received).
  • workspaceActivityPump.test.ts: descendants are tracked; each workspace's monitors, stream flag and workflow runs are posted; goal-only events and other workspaces are ignored; null resets a workspace.
  • Webview (vscode webview sub-agent tasks strip (#5109)):
    • The strip appears when the host's list gains a child: "1 sub-agent · 1 active".
    • A row click sends selectWorkspace.
    • A reported child reads "inactive" until activity reports an armed monitor; then it shows "1 active" and "Monitoring".
    • An archived child leaves the strip.
    • A worker-less active run shows "running".
  • Pre-fix: with the webview App.tsx from 🤖 feat: keep the VS Code host's workspace list live #5651, both strip tests fail (2 fail): the strip is absent.
  • Drift guard bun test ./vscode/src/webview/webviewCss.test.ts passes. No CSS files changed.
  • make check-react-compiler: 23/24, with only the existing ProjectSidebarInner skip.

Webview at 300 px (narrow VS Code sidebar). It shows nested sub-agents, a Monitoring child and a workflow group:

webview 300px

390 px:

webview 390px

900 px (same rows and labels as the desktop tray):

webview 900px

Risks

Low to medium. Desktop only sees type narrowing, and the existing tray tests pass unchanged. Activity no longer stops when the chat stream ends; it stops on deselection, file mode, refresh failure or dispose.

Size: 559 changed lines (477+, 82-), tests included. The coordinator accepted up to 560. The reason: the activity pump moved here from #5651 to keep that layer small, and this layer must also delete main's chat-tied pump (23 lines of deletions).


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $48.29

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-04T22:20:11.543794Z 97b245a PR opened
🔒 Security Review ✅ Completed 2026-10-04T22:22:35.526489Z 97b245a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33 ThomasK33 changed the title xum 5109 webview subagent dock 🤖 feat: sub-agent tasks strip in the VS Code webview dock Oct 4, 2026
@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness record

  • Final commit: 97b245a7474383bc9f145de41b0f5f5af5b382bd
  • Local validation on this head: make static-check passed; bun test ./vscode/src/ 156 pass, 0 fail; desktop SubAgentTasksDecoration and utils/ui tests 168 pass.
  • Required CI: all passing on this head (the optional Pixel / Review is ignored).
  • Review count: 3 of 6 assessments. 2 automatic (1 round, clean: Codex thumbs-up, no threads) and 1 final independent check.
  • Perf requirement 3: the selected + descendants set is recomputed only on a membership change (0 recomputes for 592 metadata events at 4,502 workspaces; numbers in the PR body).
  • Size: 559 changed lines, accepted by the coordinator up to 560 (moved activity pump plus deletion of main's chat-tied pump).
  • Final independent check: "ready with tracked follow-ups". No P1. Its non-blocking suggestions are tracked in 🤖 fix: VS Code dock activity pump follow-ups (restart after natural end, narrower task slice) #5658 (backlog). Nested-run discovery is tracked in 🤖 feat: nested workflow run discovery in the VS Code webview sub-agent strip #5652 (backlog).
  • Reason for stopping: required checks green, no review threads, final check recommends ready.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 8702e15 Oct 4, 2026
33 checks passed
@ThomasK33
ThomasK33 deleted the xum-5109-webview-subagent-dock branch October 4, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🤖 feat: sub-agent tasks strip in the VS Code webview dock

1 participant