#4014 · Project defaults lose to tab preferences

Bug · Medium priority · Low effort · ui · 2026-09-21

Issue · Base b5755d6859d68c2d8e2008fc570f2d5acec774da

REPRODUCED · root-cause confidence: high

TL;DR

The New Thread hook retains a remembered provider after project defaults arrive. The composer supplies the project provider and model, but the hook puts stored tab preferences first. A focused hook regression fails identically in two clean checkouts. This verifies selection logic; no packaged desktop instance or live provider was used.

Claims vs findings

ClaimFindingEvidence
Remembered provider overrides arriving project defaultsVerifiedBoth hook runs return global-provider instead of project-provider.
Model uses stored-first precedenceVerified by source inspectionHook lines 437–449.
Reporter database, CLI and provider healthUnverifiedNo user runtime data accessed.

Environment

Darwin arm64; Node v22.22.3; pnpm 9.15.0 via Corepack. Frozen installation succeeded. Turbo build: 58 successful tasks. A temporary pnpm wrapper was needed because the host pnpm launcher referenced a missing installation. No ports, application data directories, provider sessions, or browser were used. This is a state-selection defect, not a rendering inspection.

Minimal reproduction

  1. Check out the base commit above in a clean get-bb/bb checkout.
  2. Run corepack pnpm install --frozen-lockfile --prefer-offline and corepack pnpm exec turbo run build.
  3. Apply the regression patch.
  4. Run pnpm exec turbo run test --filter=@bb/app --force -- useThreadCreationOptions.test.tsx.

The test uses existing synthetic provider fixtures, remembers one provider, then rerenders with another project's defaults.

Expected: "project-provider"
Received: "global-provider"
Test Files  1 failed (1)
Tests  1 failed | 27 passed (28)
diff --git a/apps/app/src/hooks/useThreadCreationOptions.test.tsx b/apps/app/src/hooks/useThreadCreationOptions.test.tsx
index d59aae512..90dbcf952 100644
--- a/apps/app/src/hooks/useThreadCreationOptions.test.tsx
+++ b/apps/app/src/hooks/useThreadCreationOptions.test.tsx
@@ -294,6 +294,25 @@ afterEach(() => {
 });
 
 describe("useThreadCreationOptions", () => {
+  it("applies arriving project defaults ahead of remembered tab choices", async () => {
+    window.localStorage.setItem("bb.promptbox.provider", GLOBAL_PROVIDER_ID);
+    const { result, rerender } = renderHook(
+      ({ loaded }: { loaded: boolean }) => useThreadCreationOptions({
+        scope: "new-thread",
+        resetKey: PROJECT_ID,
+        initialProviderId: loaded ? PROJECT_PROVIDER_ID : undefined,
+        initialModel: loaded ? "project-model" : undefined,
+      }),
+      { initialProps: { loaded: false }, wrapper: createQueryClientTestHarness().wrapper },
+    );
+    await waitFor(() => expect(result.current.selectedProviderId).toBe(GLOBAL_PROVIDER_ID));
+    rerender({ loaded: true });
+    await waitFor(() => {
+      expect(result.current.selectedProviderId).toBe(PROJECT_PROVIDER_ID);
+      expect(result.current.selectedModel).toBe("project-model");
+    });
+  });
+
   it("keeps the selected remembered provider branded while models load", () => {
     window.localStorage.setItem("bb.promptbox.provider", "codex");
     writeCachedProviderList(

Root cause

NewThreadComposer passes project defaults into the hook. The hook selects storedProviderId before the supplied initial provider. Model and reasoning use similar precedence. The new-thread scope also bypasses the local-selection reset effect at lines 632–649. Together these make remembered tab state dominate project initialization.

Proposed fix

Give project defaults precedence until a picker is explicitly changed, reset that touched state when changing projects, and retain reconciled reasoning after model changes. Provider discovery when an initial provider is unavailable needs explicit regression coverage. A small attempted patch passed the new cases but failed two existing tests: reconciled reasoning after switching model ladders and root-composer provider discovery. It was reverted and not pushed; the simple-fix gate was not met.

Verification

The same agent created a second clean temporary git worktree at the exact base commit, installed its dependencies with the frozen lockfile, copied only the newly authored regression, and ran the same Turbo test command with --force. It again failed only the new test: 27 passed, 1 failed. No report correction was needed. This was a hook-level repetition, not an independent review or end-to-end desktop reproduction.

Related issues

No linked open pull requests were returned by the issue timeline or open-PR search at review time. Other reported issue relationships were not used as evidence.

Appendix

First run · Second run · Stopped fix test results

Issue content was treated as untrusted claims. No supplied scripts, patches, links, or runtime data were executed or accessed.