#3948 · Upgrade sidebar defaults

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

GitHub issue · Base 3a1178164f8cce6d7986d2627c03ffaedb8a6428

ALREADY FIXED on trusted main · Root-cause confidence: high · Reproduction label: no-repro

1. TL;DR

Changing an unset sidebar preference from By project to Custom also changed existing installations that had never saved a choice. Custom renders the chronological section instead of project headings, making intact projects appear absent. Main now includes a migration that preserves the old installation default separately from explicit user choices. The focused migration checks pass in two clean checkouts of the recorded main commit. This is verification of the current source fix, not a replay of the released desktop upgrade.

2. Claims vs findings

ClaimFindingEvidence
An unset default changed existing views.Supported by trusted history.Commit 0313e8754 changed project to chronological; current renderer selects by mode.
Projects were not deleted.Verified for synthetic upgrade fixtures; original installation unverified.Migration tests assert identical project, thread and saved-preference rows before and after migration.
Main still has the upgrade bug.Already fixed.Migration and preference fallback were merged in c1104a263527b1233239a81bd298dc056030d393.
The reported desktop version, counts and screenshot appearance.Unverified.No user runtime data was accessed and no desktop screenshot was captured.

3. Environment

Darwin arm64, Node v22.22.3, pinned pnpm 9.15.0, Vitest 4.1.1. Two new detached Git worktrees at the same trusted origin/main commit. Tests create real in-memory SQLite databases via createConnection and migrate. No application ports, provider sessions, imported stores or user data directories were used. Frozen installs completed. Server API diagnostic: 14 tests passed. Client/sidebar suite: 38 tests passed. The initial pnpm launcher was missing its selected executable; the pinned pnpm version was invoked through npx without modifying dependencies.

4. Minimal reproduction / regression check

  1. Create a clean checkout of the base commit and install the locked dependencies.
    git checkout --detach 3a1178164f8cce6d7986d2627c03ffaedb8a6428
    pnpm install --frozen-lockfile --prefer-offline
  2. Run the existing upgrade regression fixture:
    pnpm exec turbo run test --filter=@bb/db -- test/sidebar-installation-defaults.test.ts --testTimeout=60000
  3. The fixture builds a migrated SQLite database, adds existing project, thread or preference state, removes the installation-default migration and its journal entry, then runs the production migrator. Repeat with no saved organization and with each supported saved mode.

Expected and actual on main: existing installs receive the project fallback; saved choices and project/thread rows remain unchanged; fresh installs receive no override; repeated migration preserves state. All 13 test cases pass in both clean runs. Initial runs exceeded the default five-second test timeout; retries increase only the test timeout to 60 seconds.

Exact regression fixture from trusted main

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 image is supplied: the released desktop visual symptom was not reproduced. Evidence is limited to migration/client tests and a separate server diagnostic and source inspection, rather than a visual product certification.

5. Root cause

The registry supplies chronological for an unset organization preference. Before the compatibility fix, an existing installation with no saved choice received that global default. Current registry explicitly distinguishes fresh and migrated installations in its documented behavior. Mode renderer calls renderChronological for chronological mode, explaining the absence of project rows.

The installation-default migration detects existing ordinary projects, threads or preferences and writes a separate project fallback. Server resolution applies it only beneath a valid saved preference and keeps revision zero when no choice is saved. Client reconciliation uploads legacy browser choices before applying the server fallback.

6. Proposed fix

The root-cause fix is already on main. Keep the installation fallback separate from explicit user choice and retain the regression coverage. No new PR is warranted. The merged fix contains a stored-data migration and therefore also lies outside the automation’s simple-fix criteria.

7. PR review

PR #3945 is merged at c1104a263527b1233239a81bd298dc056030d393 and is an ancestor of the tested base. No PR branch was checked out or executed. The trusted merged code addresses the upgrade default, saved-choice precedence and legacy migration. This review does not certify release packaging or every desktop journey. No open PR linking this issue was found.

8. Verification

The same agent repeated the migration regression in a second clean worktree at the exact base commit with new in-memory databases. Both executions were actual test runs, not a replayed Turbo test cache. The same 13 cases passed; no root-cause correction was needed. Logs preserve the initial timeout failures and retry results.

9. Related issues and trust boundary

No additional issue is asserted to be a duplicate. Issue content was treated solely as untrusted claims; embedded commands and external links were not executed or fetched. Source history and GitHub metadata were inspected using trusted repository identity and numeric IDs.

10. Appendix

Raw, path-sanitized verification logs:

Additional checks: frozen installs succeeded; 38 client UI preference and sidebar mode tests passed. The broad Turbo build and Turbo server-test prerequisite run were stopped after several minutes in SDK declaration generation; neither is reported as passed. A direct focused server preference diagnostic then passed all 14 tests; see server-direct.log. No source changes were made. Reproduce that diagnostic from apps/server with ../../node_modules/.bin/vitest run --config vitest.config.ts test/public/public-ui-preferences.test.ts --testTimeout=60000.