Skip to content

Add settings-section and database settings-slot extension points #3312

Closed

Description

@mstomar125

Summary

Add ADR-082 §2.2's (proposed) settings extension points, so that #3327 can move Account, "Add synced database…" and the per-database sync badge out of core:

  • Settings sections. A sidebar category and its pane, placed by after, with a { navigate } prop.
  • Settings slot database.actions. Buttons in the Databases header, beside New and Open existing…. No props.
  • Settings slot database.row. Content inside each database row, with props { databaseId }.

Core's category list moves into a data module. With nothing registered, Settings renders exactly as before.

Context

Paths are under packages/desktop-app/, at af8ddb61.

src/lib/components/settings/settings-sidebar.svelte (L20-30). A hard-coded $derived list of categories:

  • database;
  • account (gated by the Labs flag syncEnabled);
  • display;
  • ai-models (Labs aiChatEnabled);
  • import ("Import Sources");
  • integrations;
  • playbooks (Labs playbooksEnabled);
  • labs;
  • about.

src/lib/components/settings/settings-pane.svelte:

  • L16-19: initialCategory handling;
  • L25-39: a fallback $effect for the three Labs-gated ids;
  • L42-64: a hard-coded {#if activeCategory === …} dispatch, with AccountSettings onNavigateToDatabase at L47-48.

src/lib/components/settings/sections/database-settings.svelte:

  • L107-116: the header button group, including the Pro button at L110-114;
  • L117-122: the explainer;
  • in each row's left column: the status line (L159) and the tenant badge (L160-171);
  • L205: the AddSyncedDatabaseDialog mount.

Who sets settingsStore.initialCategory: utils/open-settings.ts, app-shell.svelte ('integrations') and ai-chat-model-selector.svelte ('ai-models'). navigation-sidebar.svelte passes 'database'. All of these are core ids.

ADR-082 rules:

  • §2.3: order is descending priority, then registration order. A section goes after the section named in after, or before About when after is absent.
  • §2.4: when the viewed section becomes hidden, or its extension did not register, the pane falls back to Database.

Scope

1. Types and registry, in src/lib/plugins/ui-extensions.ts

export type SettingsSectionContribution =
  Contribution<{ navigate: (sectionId: string) => void }> & { label: string; after?: string };
export type SettingsSlot = 'database.actions' | 'database.row';
export type SettingsSlotContribution =
  | (Contribution & { slot: 'database.actions' })
  | (Contribution<{ databaseId: string }> & { slot: 'database.row' });
  • NodespaceExtension gains settingsSections? and settingsSlots?.
  • The registry gains two lookups:
    • settingsSections() returns Keyed sections, in registration order;
    • settingsSlotFor(slot), with a typed overload per slot, returns Keyed contributions in descending priority, with ties in registration order.
  • A section's contribution id is also its navigation id.
    • It shares one namespace with core's category ids, so after: 'database', navigate('database') and initialCategory = 'account' all work.
    • The render key is still <extension id>/<contribution id>.

2. Accessors, in src/lib/plugins/ui-extensions.svelte.ts

Add getActiveSettingsSections() and getActiveSettingsSlot(slot). Both filter through isContributionActive (#3310).

3. New src/lib/components/settings/settings-categories.ts

  • CORE_SETTINGS_CATEGORIES: the ordered core list, as { id, label, visible?: () => boolean }.
  • settingsCategories() returns the sidebar entries as { id, label, key, section? }. It builds them in four steps:
    1. Start from the full core list, hidden entries included. A section with after: 'ai-models' then keeps its place while AI Models is hidden.
    2. Drop any contributed section whose id equals a core id or the id of an earlier-registered section, and log it with log.warn. Core wins; among sections, the first registration wins.
    3. Insert each section after the entry its after names, which may be a core category or another section.
      • Sections with the same anchor go in descending priority, then registration order.
      • A section whose after is absent or unknown goes before About.
    4. Drop hidden core entries (visible() returns false) and hidden sections (when returns false or throws).
  • Also export isSettingsCategoryVisible(id) and findSettingsSection(id).

4. settings-sidebar.svelte

  • Use const categories = $derived(settingsCategories()), rendered with {#each categories as category (category.key)}.
  • Remove the labsFlags import and the Pro-specific comment. The Labs gating now lives in CORE_SETTINGS_CATEGORIES.

5. settings-pane.svelte

The fallback $effect (L31-39) becomes if (!isSettingsCategoryVisible(activeCategory)) activeCategory = 'database';. That one check covers:

  • the Labs cases;
  • an unknown or unregistered initialCategory, which today renders an empty pane;
  • a section whose when turns false while it is open.

The dispatch. After the core branches, add:

{:else if activeSection}
  {#key activeSection.key}
    <ExtensionOutlet load={activeSection.load} props={{ navigate: (id) => (activeCategory = id) }} />
  {/key}

where activeSection = $derived(findSettingsSection(activeCategory)).

Keep the AccountSettings branch. #3325 removes it.

6. database-settings.svelte

Header group. After the existing Pro {#if …}{/if} block, add:

{#each actionContributions as a (a.key)}
  <ExtensionOutlet load={a.load} />
{/each}

with const actionContributions = $derived(getActiveSettingsSlot('database.actions')).

Each row. In the left column, after the badge block and inside div.min-w-0.flex-1, add:

{#each rowContributions as r (r.key)}
  <ExtensionOutlet load={r.load} props={{ databaseId: db.id }} />
{/each}

with const rowContributions = $derived(getActiveSettingsSlot('database.row')). It is evaluated once per render, not once per row.

Pro markup. Do not touch the Pro button, the explainer, the badge or the dialog (#3324, #3326). Once #3326 deletes them, the action outlet sits right after Open existing…, and the row outlet right below the status line, which is where #3326 expects them.

7. Fixture

Add to src/tests/fixtures/test-extension/:

  • flags section, databaseActions and databaseRow;
  • options for a section's id, label, after and priority;
  • three components:
    • test-settings-section.svelte: a heading, and a button that calls navigate('database');
    • test-database-action.svelte: a "Fixture action" button;
    • test-database-row.svelte: renders row:{databaseId}.

8. Pro-boundary ratchet

Paste the printed BASELINES.

  • Removing the pane's labsFlags.syncEnabled check lowers cloudBindState.
  • The sidebar's Account line moves into settings-categories.ts, so its count is unchanged.

Out of scope

Steps

  1. Start up as core's CLAUDE.md describes: a worktree named issue-<N>-settings-extension-points, and self-assign at pickup.
  2. The types, registry and accessors, with the registry tests.
  3. settings-categories.ts and its unit tests. Then refactor the sidebar and the pane; the existing gating tests must stay green.
  4. The database-settings outlets.
  5. The fixture and the component tests, then the ratchet and the verification.

Acceptance criteria

Settings sections

  • A registered section appears at its after position: after Database with after: 'database', and before About with no after.
  • It renders when selected, whether by click or through settingsStore.initialCategory, and it receives a working navigate.
  • The pane falls back to Database for an unknown or unregistered id, and when the viewed section's when turns false.
  • A section whose id collides with a core id, or with an earlier section, is ignored and logged.

Database slots

  • database.actions contributions render once in the Databases header, and only while visible.
  • database.row contributions render once per database, with that row's databaseId.
  • A throwing row component does not break the list.

No behaviour change

  • With nothing registered, the sidebar, the pane and the database list render as before, and these tests pass unchanged: settings-pane-labs-gating, settings-pane-account-gating, settings-sidebar-labs-gating, settings-sidebar-account-gating and database-settings.
  • bun run test:changed and test:free-users pass, and quality:fix is clean.
  • bun run scripts/check-pro-boundary.ts exits 0 and BASELINES equal the printed counts, with reductions lowered.

Tests and verification

New src/tests/components/settings/settings-categories.test.ts (pure functions):

  • core order with all Labs flags on, and with all off;
  • placement by after, with a core anchor, a section anchor, a hidden anchor, an unknown anchor, and no after;
  • ties broken by priority, then registration order;
  • id collisions;
  • isSettingsCategoryVisible.

New src/tests/components/settings/settings-extension-sections.test.ts. It uses the same harness as settings-pane-labs-gating.test.ts, and checks:

  • the section's sidebar position and label;
  • it is hidden while when is false;
  • selection by click, and by initialCategory;
  • the fallback on an unregistered id, and when when turns false while the section is open;
  • navigate('database').

New src/tests/components/settings/database-settings-extensions.test.ts:

  • the header action renders only while visible;
  • row content renders once per database (row:<id>);
  • nothing renders when there are no contributors;
  • a throwing row component leaves the other rows rendered.

src/tests/plugins/ui-extensions.test.ts: lookups for the new kinds, covering ordering, keys and duplicate ids.

bun run test:changed
bun run --cwd packages/desktop-app test src/tests/components/settings src/tests/plugins
bun run --cwd packages/desktop-app test:free-users
bun run --cwd packages/desktop-app quality:fix
bun run scripts/check-pro-boundary.ts

Manual check in dev:tauri:

  • The Settings categories and the database list are unchanged.
  • The app menu's Integrations entry still opens Integrations.

Dependencies

Risks and rollback

  • Fallback change. An unknown category now shows Database instead of an empty pane. Every current caller passes a valid id.
  • Ordering regression. Guarded by the unchanged gating tests and the new ordering tests.
  • Rollback. Revert the PR. With nothing registered, the change is purely additive.

Plan key: C3.3 (pro-boundary DAG).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

pro-boundaryPro/core boundary: Pro code moves to nodespace-sync (ADR-081..085)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions