← reports

#3160 · Inline workflow rows omit child-thread navigation

Bug Medium Effort: Low ui workflows open on GitHub 2026-09-05 · base 6cdb4ba61255

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A workflow's inline message card displays child-agent rows but does not make rows with a child thread clickable or keyboard-focusable. The same data rendered in the workflow side panel does navigate to the child thread. A focused component regression failed identically in two clean checkouts of the trusted current main commit: Testing Library could not find an actionable inline worker as a button. The inline card omits the activation callback that the shared row component requires before it renders an actionable agent as a button.

2. Claims vs findings

ClaimStatusEvidence
An inline child-agent row with a child thread cannot be activated.VerifiedThe focused test could find the agent text but not a button with that accessible name in either clean checkout.
The workflow side panel supports child-thread navigation.VerifiedThe existing panel component test passed and recorded a toThread call for the same fixture shape.
The child-thread identifier is missing from workflow view data.RefutedThe workflow view maps a non-null child identifier to actionable: true and retains the call in callsById.
Rows without a child thread should remain inert.Verified staticallyThe mapping marks them non-actionable, and the shared row renders a div unless both actionability and an activation callback are present.

3. Environment

4. Minimal reproduction

  1. At the trusted base commit, replace plugins/workflows/src/app.test.tsx with the linked test artifact, which adds the regression below.
  2. Run:
    cd plugins/workflows
    pnpm exec vitest run --config vitest.config.ts src/app.test.tsx -t "opens actionable worker rows and leaves rows without a child thread inert"
  3. Expected:
    Test Files  1 passed (1)
    Tests       1 passed | 20 skipped (21)
  4. Actual:
    TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/adversarial review/i`
    
    Test Files  1 failed (1)
    Tests       1 failed | 20 skipped (21)
    exit status: 1

Repro files: test file, first clean failure, second clean failure.

Focused regression

it("opens actionable worker rows and leaves rows without a child thread inert", async () => {
  const runWithInertWorker: WorkflowRunView = {
    ...run,
    phases: run.phases.map((phase) => ({
      ...phase,
      calls: phase.calls.map((call) =>
        call.id === "wfc_1" ? { ...call, childThreadId: null } : call,
      ),
    })),
  };
  const slot = renderSlot(
    app.messageDirectives[0]!,
    {
      attributes: { run: run.id },
      source: `::workflow-preview{run="${run.id}"}`,
      message,
      openWorkspaceFile: null,
    },
    { rpc: { workflowRunView: () => ({ run: runWithInertWorker }) } },
  );

  await slot.findByText("Adversarial review");
  fireEvent.click(slot.getByRole("button", { name: /adversarial review/i }));
  expect(slot.navigateCalls).toContainEqual({
    method: "toThread",
    threadId: "thr_worker_2",
  });

  fireEvent.click(slot.getByRole("button", { name: /Discover1\/1/ }));
  const inertWorker = slot.getByText("Inspect implementation").parentElement;
  expect(inertWorker?.tagName).toBe("DIV");
});

Second clean verification

A second repository clone was checked out at the exact same base SHA, installed with the frozen lockfile, and fully built. The identical test then failed at the same query for the inline worker button with exit status 1. The second run supported every report claim, so no correction was needed.

5. Root cause

buildSharedWorkflowView correctly marks calls with a child thread as actionable and preserves a call lookup by identifier. The data boundary is therefore intact.

The inline card's WorkflowProgress rendering passes progress and presentation state but no onAgentActivate. By contrast, the side panel supplies that callback, resolves the child identifier through callsById, and calls navigate.toThread.

<WorkflowProgress
  progress={shared.progress}
  settled={!isRunActive(run)}
  error={run.error}
  collapsiblePhases
  currentPhaseIndex={shared.currentPhaseIndex}
  terminalState={runTerminalState(run)}
/>

The shared UI deliberately requires both conditions: phase groups only attach activation when a callback exists and the agent is actionable, and WorkflowAgentLine otherwise renders a non-focusable div. The missing inline callback directly explains the absent hover, focus, keyboard, click, and navigation behavior.

6. Proposed fix (first principles)

Give the inline WorkflowProgress the same child-call resolution and navigate.toThread behavior as the panel. Keep the guard on a non-null child identifier so non-actionable rows remain inert. Reuse a small local activation helper between both surfaces to prevent their navigation rules from drifting, then retain the focused component regression for actionable and inert rows.

7. Related issues

No linked pull request was present in GitHub metadata. Repository search found nearby child-thread and workflow UI reports, but none covered this same first-party inline-versus-panel callback mismatch.

8. Appendix

The issue title, body, comments, links, attachments, and code blocks were treated as untrusted claims. No external link, issue-provided command, patch, branch, binary, script, or workflow was opened or executed.

Commands run

git clone --filter=blob:none --branch main https://github.com/get-bb/bb.git target-a
git clone --filter=blob:none --branch main https://github.com/get-bb/bb.git target-b
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec vitest run --config vitest.config.ts src/app.test.tsx
pnpm exec vitest run --config vitest.config.ts src/app.test.tsx -t "opens actionable worker rows and leaves rows without a child thread inert"