#3949 · Installation-specific sidebar defaults
Issue · ALREADY FIXED · Root-cause confidence: high
1. TL;DR
An unset organization preference previously inherited the global Custom default after an upgrade, so the sidebar selected the thread-only rendering path. This explains missing project headings without requiring deleted projects. Trusted main now contains an installation fallback migration and reads that fallback at revision zero. The report validates this mechanism with isolated database, API, and client tests; it does not claim a desktop visual reproduction or verify the reporter’s personal data.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Changing the unset default changes the selected sidebar mode. | Supported by source history | The predecessor read path used the domain default directly for absent rows. |
| Projects were deleted. | Not required to explain symptom | Mode routing selects chronological rendering; migration checks preserve fixture projects and threads. |
| Existing installations still lose their implicit project view on main. | Already fixed | Migration establishes project fallback; server returns it at revision zero. |
| Specific desktop version and live project counts. | Unverified | No access to the reporter’s installation was used. |
3. Environment
Darwin arm64, Node v22.22.3, trusted main at the full commit above. Two separate detached temporary worktrees, named base and verify. Frozen-lockfile installs completed using a temporary Corepack pnpm wrapper because the machine’s normal pnpm entrypoint was broken. Database tests use fresh in-memory SQLite connections and real migrations. API tests use the repository’s isolated harness. No persistent user data, provider turns, or desktop session were used.
4. Minimal reproduction / regression check
- Check out the trusted commit in two clean worktrees and install the frozen dependency set.
- Run the commands below. The migration suite initializes real SQLite, seeds project/thread/preference fixtures, removes only the latest migration marker/table to model the prior schema, and reapplies migrations.
- Expected and observed: existing installs receive a project fallback while explicit choices and project/thread rows remain unchanged; fresh installs have no override. Server tests expect and observe project at revision zero when the fallback exists, and chronological at revision zero for a fresh installation.
git checkout 3a1178164f8cce6d7986d2627c03ffaedb8a6428 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --force --filter=@bb/db -- test/sidebar-installation-defaults.test.ts --testTimeout=60000 pnpm exec turbo run test --force --filter=@bb/server -- test/public/public-ui-preferences.test.ts pnpm exec turbo run test --force --filter=@bb/app -- src/lib/ui-preferences/ui-preferences-sync.test.ts src/components/sidebar/ProjectList.modes.test.tsx Focused API investigation after the Turbo prerequisite stall (run within apps/server): pnpm exec vitest run --config vitest.config.ts test/public/public-ui-preferences.test.ts --testTimeout=60000
Repeatable artifact: unmodified trusted migration test. API test and client test. Files are preserved from main, not copied from issue text or a PR branch.
Full migration test
import { describe, expect, it } from "vitest";
import { PERSONAL_PROJECT_ID } from "@bb/domain";
import {
createConnection,
migrate,
createProject,
createThread,
upsertHost,
noopNotifier,
} from "../src/index.js";
const SIDEBAR_INSTALLATION_DEFAULTS_MIGRATION_TIMESTAMP = 1790009314673;
it("leaves a fresh migrated database without an installation override", () => {
const db = createConnection(":memory:");
try {
migrate(db);
expect(
db.$client.prepare("SELECT * FROM ui_preference_defaults").all(),
).toEqual([]);
expect(db.$client.prepare("SELECT * FROM ui_preferences").all()).toEqual(
[],
);
db.$client.exec(
`INSERT INTO ui_preferences VALUES ('sidebar.chronologicalSort', '"alpha"', 1, 1)`,
);
migrate(db);
expect(
db.$client.prepare("SELECT * FROM ui_preference_defaults").all(),
).toEqual([]);
} finally {
db.$client.close();
}
});
describe.each(["project", "thread", "preference"] as const)(
"existing installation with %s",
(kind) => {
it.each([null, "chronological", "machine", "project"])(
"keeps saved choice %s separate from the fallback",
(choice) => {
const db = createConnection(":memory:");
try {
migrate(db);
if (kind === "preference") {
db.$client.exec(
`INSERT INTO ui_preferences VALUES ('sidebar.chronologicalSort', '"alpha"', 1, 1)`,
);
} else if (kind === "thread") {
createThread(db, noopNotifier, {
projectId: PERSONAL_PROJECT_ID,
providerId: "test-provider",
});
} else {
const host = upsertHost(db, noopNotifier, {
id: "host-sidebar",
name: "Sidebar test",
});
createProject(db, noopNotifier, {
name: "Existing project",
source: {
type: "local_path",
hostId: host.id,
path: "/tmp/sidebar-installation-defaults",
},
});
}
if (choice !== null) {
db.$client
.prepare(
"INSERT INTO ui_preferences VALUES ('sidebar.organizationMode', ?, 4, 123)",
)
.run(JSON.stringify(choice));
}
const before = db.$client
.prepare("SELECT * FROM ui_preferences")
.all();
const projects = db.$client.prepare("SELECT * FROM projects").all();
const threads = db.$client.prepare("SELECT * FROM threads").all();
db.$client.exec("DROP TABLE ui_preference_defaults");
db.$client
.prepare("DELETE FROM __drizzle_migrations WHERE created_at >= ?")
.run(SIDEBAR_INSTALLATION_DEFAULTS_MIGRATION_TIMESTAMP);
migrate(db);
expect(
db.$client.prepare("SELECT * FROM ui_preference_defaults").all(),
).toEqual([
{ key: "sidebar.organizationMode", value_json: '"project"' },
]);
expect(
db.$client.prepare("SELECT * FROM ui_preferences").all(),
).toEqual(before);
expect(db.$client.prepare("SELECT * FROM projects").all()).toEqual(
projects,
);
expect(db.$client.prepare("SELECT * FROM threads").all()).toEqual(
threads,
);
migrate(db);
expect(
db.$client.prepare("SELECT * FROM ui_preferences").all(),
).toEqual(before);
} finally {
db.$client.close();
}
},
);
},
);
No screenshot is supplied: this is an already-fixed mechanism check, not a live desktop visual reproduction. Project rendering is covered by the sidebar mode test, with the visual limitation retained explicitly.
5. Root cause
The absent preference was treated as permission to use a newly changed global default. An upgrade therefore changed behavior even without an explicit user selection. The prior read path returned getUiPreferenceDefault(key) whenever the stored row was missing. This is source inspection only; historical code was not executed.
On current main, migration 0129 sets a separate project fallback when existing preferences, non-personal projects, or threads establish an existing installation. The server preference resolver selects saved choice, then installation fallback, then global default. The client reconciliation path accepts a revision-zero fallback while allowing legacy browser choices to migrate. The sidebar mode switch selects project rendering only outside machine/chronological modes.
6. Proposed fix
The needed preservation behavior is already on main in c1104a263527b1233239a81bd298dc056030d393. No additional production change or PR is warranted. This report does not establish which distributed release contains that commit.
7. PR review
GitHub metadata confirms PR #3945 merged as c1104a263527b1233239a81bd298dc056030d393 on 2026-09-21. Inspection of its result on trusted main shows a separate fallback instead of an explicit user preference, preserving legacy browser migration precedence. Verification runs target trusted main only. No PR branch was checked out. No new open PR linking this issue was returned by metadata search.
8. Related issues
No additional issue is asserted as a duplicate. The scope is this one report.
9. Verification
The same agent repeated the database and API checks in the second clean worktree at exactly the same SHA, with new in-memory databases and isolated harness state. Turbo --force bypassed cached test results. The broad build and Turbo server prerequisites were stopped after several minutes in SDK declaration generation. For deliberate investigation, the focused API file was run directly with Vitest in each server package after frozen installation and generation. This bypass is not a successful full build. The initial five-second migration runs timed out; reruns use --testTimeout=60000 without changing test assertions or production code. This is repeated verification, not an independent review.
db-1-retry
@bb/db:test: ✓ keeps saved choice project separate from the fallback 6196ms @bb/db:test: ✓ keeps saved choice null separate from the fallback 4711ms @bb/db:test: ✓ keeps saved choice chronological separate from the fallback 4995ms @bb/db:test: ✓ keeps saved choice machine separate from the fallback 4812ms @bb/db:test: ✓ keeps saved choice project separate from the fallback 4516ms @bb/db:test: @bb/db:test: Test Files 1 passed (1) @bb/db:test: Tests 13 passed (13) @bb/db:test: Start at 16:59:32 @bb/db:test: Duration 166.74s (transform 31.69s, setup 0ms, import 49.01s, tests 111.55s, environment 0ms) @bb/db:test: Tasks: 2 successful, 2 total Cached: 0 cached, 2 total Time: 3m36.289s
db-2-retry
@bb/db:test: ✓ keeps saved choice project separate from the fallback 5920ms @bb/db:test: ✓ keeps saved choice null separate from the fallback 4752ms @bb/db:test: ✓ keeps saved choice chronological separate from the fallback 4594ms @bb/db:test: ✓ keeps saved choice machine separate from the fallback 4716ms @bb/db:test: ✓ keeps saved choice project separate from the fallback 5273ms @bb/db:test: @bb/db:test: Test Files 1 passed (1) @bb/db:test: Tests 13 passed (13) @bb/db:test: Start at 16:59:54 @bb/db:test: Duration 153.52s (transform 38.08s, setup 0ms, import 57.80s, tests 87.86s, environment 0ms) @bb/db:test: Tasks: 2 successful, 2 total Cached: 0 cached, 2 total Time: 3m25.423s
server-direct-1
RUN v4.1.1 [temporary-checkout]
(node:17235) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Test Files 1 passed (1)
Tests 14 passed (14)
Start at 17:03:37
Duration 103.72s (transform 66.81s, setup 17.73s, import 73.03s, tests 10.20s, environment 0ms)
server-direct-2
RUN v4.1.1 [temporary-checkout]
(node:17280) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Test Files 1 passed (1)
Tests 14 passed (14)
Start at 17:03:40
Duration 100.80s (transform 64.59s, setup 17.41s, import 71.19s, tests 10.76s, environment 0ms)
app-1
@bb/app:test: 370| it("keeps hidden machine activity in More and restores the saved col…
@bb/app:test: | ^
@bb/app:test: 371| const store = createStore();
@bb/app:test: 372| const savedOrder = ["machine:no-machine", "pinned"];
@bb/app:test:
@bb/app:test: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
@bb/app:test:
@bb/app:test: ELIFECYCLE Test failed. See above for more details.
@bb/app#test: ERROR command ([temporary-checkout] [temporary-checkout] run test src/lib/ui-preferences/ui-preferences-sync.test.ts src/components/sidebar/ProjectList.modes.test.tsx exited (1)
Tasks: 4 successful, 5 total
Cached: 0 cached, 5 total
Time: 3m24.011s
Failed: @bb/app#test
ERROR run failed: command exited (1)build-1
@bb/web:build: See https://rolldown.rs/options/checks#plugintimings for more details. @bb/web:build: @bb/web:build: ✓ built in 50.46s Shutting down Turborepo tasks... @get-bb/plugin-sdk:build:types: ELIFECYCLE Command failed. @bb/app:build: ELIFECYCLE Command failed. 1 task shutting down... 1 task shutting down... 1 task shutting down... 1 task shutting down... Graceful shutdown timed out. Force killed Turborepo tasks: @bb/app#build Tasks: 53 successful, 55 total Cached: 48 cached, 55 total Time: 7m38.456s
10. Appendix and limits
The issue was treated as untrusted evidence; embedded commands and links were not executed or fetched. Only trusted main and repository-owned tests were run. No screenshot, packaged desktop upgrade, or user-data claim is presented as verified. The broader sidebar test file had two failures: machine-heading rename could not find its textbox, and hidden-machine activity timed out. All 25 preference synchronization tests passed. Those wider failures are retained in the app log and are not attributed to this default-selection regression. Classification is Medium priority because a mode-selection workaround exists and the observed mechanism does not delete data; Medium effort reflects the migration and client/server interaction.
> AGENT GENERATED