#4150 · Legacy sidebar preference normalization changes provider selection
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high
TL;DR
The current preference code selects the bundled thread list for both an unset preference and a stored Automatic value. A focused test with community and bundled registrations reproduces the change even when the community registration is first. Explicit community selections survive. This establishes the selection mechanism, but does not reproduce the published-package upgrade, actual third-party plugin loading, browser rendering, or absence of an upgrade notice.
Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Unset and Automatic preferences choose the bundled provider | Verified at unit level | Two expected continuity failures in each checkout |
| Explicit selection restores community provider | Verified at unit level | Passing explicit-selection control in each checkout |
| The package upgrade silently changes the visible sidebar while the plugin remains running | Unverified end to end | No package upgrade, browser session, or third-party code executed |
| Documentation describes obsolete selection rules | Verified statically | Documentation lines 82–95 still describe Automatic and localStorage |
Environment
Darwin arm64; Node v22.22.3. Two separate detached worktrees at the recorded main commit, each with a frozen dependency install. No provider, database, server, ports, or runtime data were used. The full base build passed: 60 tasks successful. The host pnpm entrypoint was broken; a temporary pnpm shim executed corepack pnpm, with no repository dependency changes.
Minimal reproduction
- Create a clean trusted checkout at the base commit and install dependencies using
pnpm install --frozen-lockfile --prefer-offline. - Save the inline test below as
packages/domain/test/issue-4150.test.ts. - Run
pnpm exec turbo run test --filter=@bb/domain -- --run test/issue-4150.test.ts.
Expected by the continuity assertions: community registration remains selected. Actual output in both runs:
unset preference: thread-list/thread-list {"kind":"plugin","registration":{"pluginId":"thread-list","id":"thread-list"}}
legacy preference: thread-list/thread-list {"kind":"plugin","registration":{"pluginId":"thread-list","id":"thread-list"}}
Tests 2 failed | 1 passed (3)
The test executes the real domain parser/default getter and replacement resolver. Its small selection adapter mirrors the preference-to-predicate conversion; it does not mount the production React hook or test server persistence. The continuity assertions deliberately disagree with current policy. No screenshots are provided because visual behavior was not tested.
import { describe, expect, it } from "vitest";
import { getUiPreferenceDefault, parseUiPreferenceValue } from "../src/ui-preferences";
import { resolveReplacement } from "../../../apps/app/src/lib/plugin-slot-resolvers";
const key = "sidebar.threadListProvider";
const community = { pluginId: "qa-sidebar", id: "list" };
const bundled = { pluginId: "thread-list", id: "thread-list" };
const registrations = [community, bundled];
function selected(preference: string) {
return resolveReplacement(registrations, preference === "__automatic__"
? undefined
: (slot) => `${slot.pluginId}/${slot.id}` === preference);
}
describe("issue 4150 preference continuity", () => {
it("preserves the automatic provider for an unset preference", () => {
const preference = getUiPreferenceDefault(key);
console.log("unset preference:", preference, JSON.stringify(selected(preference)));
expect(selected(preference)).toEqual({ kind: "plugin", registration: community });
});
it("preserves the automatic provider for a stored legacy value", () => {
const parsed = parseUiPreferenceValue(key, "__automatic__");
expect(parsed.success).toBe(true);
if (!parsed.success) throw new Error(parsed.message);
console.log("legacy preference:", parsed.value, JSON.stringify(selected(parsed.value)));
expect(selected(parsed.value)).toEqual({ kind: "plugin", registration: community });
});
it("preserves an explicit community provider", () => {
const parsed = parseUiPreferenceValue(key, "qa-sidebar/list");
expect(parsed.success).toBe(true);
if (!parsed.success) throw new Error(parsed.message);
expect(selected(parsed.value)).toEqual({ kind: "plugin", registration: community });
});
});
Root cause
The preference definition defaults to thread-list/thread-list and normalizes legacy Automatic and built-in values to it. The server read path uses this default when no stored value exists and parses stored values through that definition. The sidebar hook consumes the synced preference; the preference adapter converts the explicit key to a matching predicate; the resolver chooses the matching registration instead of the first registration.
Trusted main history records the deliberate default-policy change in commit 74e476f3fbd31fbc6ff33219274e29c3d663cfee. Consequently this is an upgrade-continuity gap in a deliberate policy, not an accidental failure to load a plugin. The documentation still describes the previous policy.
Verification
The same agent repeated the test in a second clean detached worktree at the exact recorded commit, with a separate frozen install. The command and output were identical: two continuity assertions failed and the explicit-selection control passed. Both Turbo runs executed without test cache hits. No correction to the unit-level finding was necessary. This is repeated verification by the same agent, not independent review.
Proposed fix and safety decision
Choose an explicit upgrade-continuity policy: preserve a previously active community provider via a migration, or explain the changed selection with a notice. Update the obsolete documentation alongside that behavior. A migration needs stored-data changes and reliable knowledge of the previous provider; a notice requires a product decision about timing and dismissal. Both exceed this automation's simple-fix conditions. No production fix, branch, or pull request was created.
Related issues and pull requests
No open pull request linking #4150 was found through closing-reference metadata or an open-PR number search. Sidebar visual layout issues do not establish this selection regression; no duplicate was identified in the small sidebar issue sample.
Appendix
Raw logs and the test file are retained in the local report backup; the complete reproduction test and decisive output appear inline above, per the reports repository publication rules. Commands used: git fetch origin main; git worktree add --detach for each checkout; frozen pnpm install; Turbo build; the focused Turbo test command above; read-only GitHub metadata and source/history inspection. A final fetch showed the same main commit.
Issue text and embedded commands were treated as untrusted evidence. No issue-provided command, external link, third-party plugin code, or pull-request branch was executed.