Repository navigation
Description
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:
initialCategoryhandling; - L25-39: a fallback
$effectfor the three Labs-gated ids; - L42-64: a hard-coded
{#if activeCategory === …}dispatch, withAccountSettings onNavigateToDatabaseat 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
AddSyncedDatabaseDialogmount.
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 whenafteris 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' });NodespaceExtensiongainssettingsSections?andsettingsSlots?.- The registry gains two lookups:
settingsSections()returnsKeyedsections, in registration order;settingsSlotFor(slot), with a typed overload per slot, returnsKeyedcontributions in descending priority, with ties in registration order.
- A section's contribution
idis also its navigation id.- It shares one namespace with core's category ids, so
after: 'database',navigate('database')andinitialCategory = 'account'all work. - The render key is still
<extension id>/<contribution id>.
- It shares one namespace with core's category ids, so
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 }.- Move today's labels and Labs gates here verbatim.
- Account stays a core entry, with
visible: () => labsFlags.syncEnabled, until Delete the Pro Account section, workspace-members card and tenant-admission calls #3325 removes it.
settingsCategories()returns the sidebar entries as{ id, label, key, section? }. It builds them in four steps:- Start from the full core list, hidden entries included. A section with
after: 'ai-models'then keeps its place while AI Models is hidden. - 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. - Insert each section after the entry its
afternames, which may be a core category or another section.- Sections with the same anchor go in descending priority, then registration order.
- A section whose
afteris absent or unknown goes before About.
- Drop hidden core entries (
visible()returns false) and hidden sections (whenreturns false or throws).
- Start from the full core list, hidden entries included. A section with
- Also export
isSettingsCategoryVisible(id)andfindSettingsSection(id).
4. settings-sidebar.svelte
- Use
const categories = $derived(settingsCategories()), rendered with{#each categories as category (category.key)}. - Remove the
labsFlagsimport and the Pro-specific comment. The Labs gating now lives inCORE_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
whenturns 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,databaseActionsanddatabaseRow; - options for a section's
id,label,afterandpriority; - three components:
test-settings-section.svelte: a heading, and a button that callsnavigate('database');test-database-action.svelte: a "Fixture action" button;test-database-row.svelte: rendersrow:{databaseId}.
8. Pro-boundary ratchet
Paste the printed BASELINES.
- Removing the pane's
labsFlags.syncEnabledcheck lowerscloudBindState. - The sidebar's Account line moves into
settings-categories.ts, so its count is unchanged.
Out of scope
- Moving Account,
WorkspaceMembersCard,AddSyncedDatabaseDialogor the badge onto these points (Delete the Pro Account section, workspace-members card and tenant-admission calls #3325, Delete the Add synced database dialog and tenant badge from Database settings #3326). - Changing the explainer (Remove tenant and sync wording shown to free users in Database settings #3324).
- Removing the Labs
syncEnabledgate (Add the "Share / Sync this database" entry point that opens nodespace.ai #3362). - The replaceable
database.share-actionslot in the ADR-082 draft, which is pending ADR-084 decision 7. See the parent's open questions.
Steps
- Start up as core's
CLAUDE.mddescribes: a worktree namedissue-<N>-settings-extension-points, and self-assign at pickup. - The types, registry and accessors, with the registry tests.
settings-categories.tsand its unit tests. Then refactor the sidebar and the pane; the existing gating tests must stay green.- The database-settings outlets.
- The fixture and the component tests, then the ratchet and the verification.
Acceptance criteria
Settings sections
- A registered section appears at its
afterposition: after Database withafter: 'database', and before About with noafter. - It renders when selected, whether by click or through
settingsStore.initialCategory, and it receives a workingnavigate. - The pane falls back to Database for an unknown or unregistered id, and when the viewed section's
whenturns false. - A section whose id collides with a core id, or with an earlier section, is ignored and logged.
Database slots
-
database.actionscontributions render once in the Databases header, and only while visible. -
database.rowcontributions render once per database, with that row'sdatabaseId. - 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-gatinganddatabase-settings. -
bun run test:changedandtest:free-userspass, andquality:fixis clean. -
bun run scripts/check-pro-boundary.tsexits 0 andBASELINESequal 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 noafter; - 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
whenis false; - selection by click, and by
initialCategory; - the fallback on an unregistered id, and when
whenturns 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
- Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310: the contribution contract,
isContributionActive, the fixture and the isolatedExtensionOutlet.
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
Activity
mstomar125 commentedon Sep 30, 2026
Completed in #3392 (merged). Auto-close did not fire; closing manually.