Repository navigation
Add the replaceable database.sync-entry action slot to the extension API #3358
Description
Summary
Add a replaceable action slot, database.sync-entry, to core's frontend extension API (ADR-082 §2.2, proposed; ADR-084 §1.1, proposed). Host it as the last item of the Databases header action row in Settings → Database, with an empty default for now. Core looks and behaves exactly as before.
#3359 later fills the default with core's website action. An extension, such as the Pro package, can then do one of two things:
- contribute its own action to the slot (ADR-084 decision 7(a));
- register an always-hidden contribution, so the place stays empty (7(b)).
This is an ADR-081 §7 step 2 extension point, so it is additive. It lands before the entry action so the Pro package can write and test its contribution against a real core revision. ADR-081 §7 step 7 requires that before core ships the action.
Context
What #3317 provides. Names follow ADR-082 §2. If #3317 merged different names, use #3317's.
- Registry.
packages/desktop-app/src/lib/plugins/ui-extensions.tsholds the types.ui-extensions.svelte.tsholds the reactivegetActive*accessors. - Contributions carry:
id;- an optional reactive
when(). A throwingwhen()counts as false and is logged once; priority;- a lazy
load.
- Outlet.
extension-outlet.svelterenders a contribution, with a{:catch}branch and a<svelte:boundary>. It passes props, as thedatabase.rowhost from Add settings-section and database settings-slot extension points #3312 does. - Host API barrel.
@nodespace/extension-api(src/lib/extension-api/index.ts, Publish the extension host API: services, ui and testing entries, version policy #3315):- re-exports the registration types and
EXTENSION_API_VERSION; - has an export snapshot test;
- is guarded by a test that forbids core modules from importing it.
- re-exports the registration types and
- Test fixture. Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 / Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310 adds a fixture extension under
src/tests/fixtures/. - The header row. Add settings-section and database settings-slot extension points #3312 mounts the
database.actionsoutlets in the Databases header action row ofdatabase-settings.svelte: the<div class="flex gap-2">beside "New" and "Open existing…" (lines 105-116 ataf8ddb61). Delete the Add synced database dialog and tenant badge from Database settings #3326 removes "Add synced database…" and theproSyncandlabsFlagsimports from that row.
The rules for a replaceable slot (ADR-082 §2.2, §2.3, §2.4 and §8):
-
Core renders its default only while no registered extension contributes to the slot. This depends on registration, not on visibility.
-
Once a contribution is registered, core's default is not rendered, even when that contribution:
- is hidden (
when()is false); - has a
when()that throws; - has a
load()that rejects; - has a component that throws.
The place then stays empty.
- is hidden (
-
At most one contribution renders in each place: the visible one with the highest
priority, then the first registered. -
The rendered contribution is keyed by
<extension id>/<contribution id>, so a visibility change mounts or unmounts only that outlet. -
The props are
{ databaseId: string; placement: 'settings' | 'menu' }. Core's default ignoresdatabaseId, because ADR-084 §1 builds the URL from constants only. -
The slot is presentation only. No access decision depends on what fills it.
The slot id. ADR-084 §1.1 and §9 call the slot database.sync-entry. The ADR-082 §2.2 draft calls it database.share-action, under an actionSlots field. The id is part of the versioned extension API (ADR-082 §7), so the two merged ADRs must agree before you start. This issue uses database.sync-entry. If the merged ADR-082 uses another id, use that id everywhere below.
Why the header row. Core shows one action per page, for the active database. A per-database indicator belongs in the database.row slot (#3312), not here.
Scope
-
Types. Add them to the registry types module (
ui-extensions.ts, or wherever Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317 keeps the contribution types):export const DATABASE_SYNC_ENTRY_SLOT = 'database.sync-entry' as const; export interface ActionSlotProps { databaseId: string; placement: 'settings' | 'menu' } export type ActionSlotContribution = Contribution<ActionSlotProps> & { slot: typeof DATABASE_SYNC_ENTRY_SLOT }; // on NodespaceExtension (C3's extension definition type): actionSlots?: ActionSlotContribution[];
Registration applies Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317's existing rules: contribution ids are unique within an extension, and a duplicate extension id is logged and dropped.
-
Accessors. Add them to
ui-extensions.svelte.ts, reactive in the same way as the othergetActive*accessors:hasActionSlotContribution(slot): booleanreturns true when any registered extension has a contribution forslot, whatever itswhen()returns.getActiveActionSlotContribution(slot): { key: string; contribution: ActionSlotContribution } | nullreturns the visible contribution with the highestpriority, with ties in registration order. It uses Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317's throw-safewhen()helper.keyis<extension id>/<contribution id>.
-
New host,
src/lib/plugins/action-slot-host.svelte. Its props areslot,props: ActionSlotPropsandfallback: Snippet.- With no contribution registered, it runs
{@render fallback()}. - Otherwise it renders the active contribution, if there is one, through
ExtensionOutletwithprops, inside{#key key}. It renders nothing when no contribution is visible. - The module doc states the rules above and cites ADR-082 §2.2 and ADR-084 §1.1. It uses no Pro, tenant or sync-product wording.
- With no contribution registered, it runs
-
src/lib/components/settings/sections/database-settings.svelte. In the Databases header action row, after thedatabase.actionsoutlet, add:{#if activeDatabaseId} <ActionSlotHost slot={DATABASE_SYNC_ENTRY_SLOT} props={{ databaseId: activeDatabaseId, placement: 'settings' }} fallback={syncEntryDefault} /> {/if} {#snippet syncEntryDefault()}{/snippet}The snippet stays empty until Make the Share / Sync website action the sync-entry default; remove the Labs card #3359.
-
Host API (
src/lib/extension-api/index.ts,@nodespace/extension-api):- Re-export
DATABASE_SYNC_ENTRY_SLOT,ActionSlotContributionandActionSlotProps. - List the slot in the module doc, with its host location (the last item of the Settings → Database header row), its props and its mount semantics.
- Bump
EXTENSION_API_VERSION.minor. Under ADR-082 §7 a new optional slot is additive, which means a minor bump. If Split the desktop Tauri crate and add ADR-082's Tauri extension points #3322 has added the RustEXTENSION_API_VERSIONtuple and its equality test, bump the tuple too. - Update Publish the extension host API: services, ui and testing entries, version policy #3315's export snapshot test, and its type-declaration snapshot if it records one (ADR-082 §7).
- Re-export
-
Fixture extension (Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 / Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310's, under
src/tests/fixtures/). Add adatabase.sync-entrycontribution:- its component renders
<button>fixture-sync-entry:{databaseId}:{placement}</button>; - its
whenis driven by a$stateflag the tests control.
Keep the fixture covering every slot kind (ADR-082 §8).
- its component renders
Out of scope
- Core's default content: the button, the dialog and the URL (Make the Share / Sync website action the sync-entry default; remove the Labs card #3359).
- The native File menu entry (Add a File menu item that opens the Share / Sync this database entry #3360).
- The Pro package's contribution (nodespace-sync).
- A "Share this node" slot. ADR-084 defers that action, and its slot is a later additive change.
Steps
- Run the startup sequence from CLAUDE.md:
- on the primary checkout:
git status, thengit fetch origin && git pull origin main; EnterWorktree({name: "issue-<N>-sync-entry-slot"});- inside the worktree:
bun install,bun run test(baseline),bun run gh:comment <N> "Frontend: X passed",bun run gh:assign <N> "@me",bun run gh:status <N> "In Progress".
- on the primary checkout:
- Pre-checks:
- ADR-082 is merged, and its §2.2 slot id matches ADR-084 §1.1. Check nodespace-docs
origin/main, never a local checkout:git -C <docs> fetch origin main git -C <docs> show origin/main:decisions/<082 file> | grep -n "sync-entry\|share-action"
- Publish the extension host API: services, ui and testing entries, version policy #3315 and Delete the Add synced database dialog and tenant badge from Database settings #3326 are merged. The host API barrel exists, and
git grep -n "Add synced database" -- packages/desktop-app/src/lib/components/settings/sections/database-settings.sveltereturns nothing.
- ADR-082 is merged, and its §2.2 slot id matches ADR-084 §1.1. Check nodespace-docs
- Add the types, the accessors and the host, together with
src/tests/plugins/action-slot-host.test.ts. - Mount the host in
database-settings.svelte, and add its tests todatabase-settings.test.ts. - Export the slot from the host API, bump the version, and update the snapshot and the fixture.
- Finish:
bun run test:changed;bun run scripts/check-pro-boundary.ts;bun run --cwd packages/desktop-app quality:fix, then commit;git push origin HEAD:issue-<N>-sync-entry-slot(lint only);gh pr create --repo NodeSpaceAI/nodespace-core --base main --head issue-<N>-sync-entry-slot. In the PR description, name the new slot and the minor bump under an "Extension API" heading.- After the user approves:
bun run merge <PR#>in the background, thenExitWorktree({action: "remove", discard_changes: true}).
Standards:
- Use
createLogger, neverconsole.*. - No lint suppression, no
any, no{@html}. - Use
bun run test, neverbun test. - Mock Tauri with
vi.mock(...). - Core modules import the registry directly, never
@nodespace/extension-api. - Code comments cite ADR-082 and ADR-084, never issue numbers.
Acceptance criteria
- With no extension registered, Settings → Database renders exactly as before: the header row holds "New" and "Open existing…" and nothing else. This is every core build, since
virtual:nodespace-extensionsis empty. - A registered, visible contribution renders in the header row after the
database.actionsoutlets, with{ databaseId: <active id>, placement: 'settings' }. The default snippet does not render. - A registered contribution that is hidden, whose
when()throws, whoseload()rejects, or whose component throws leaves the place empty, and the default snippet does not render. - With two contributions, the higher
prioritywins, and a tie goes to the first registered. - Toggling a contribution's
when()mounts and unmounts only its outlet. -
@nodespace/extension-apiexportsDATABASE_SYNC_ENTRY_SLOT,ActionSlotContributionandActionSlotProps. -
EXTENSION_API_VERSION.minoris bumped, and so is the Rust tuple if it exists. The export snapshot is updated. - The host API module doc documents the slot's id, host location, props and mount semantics.
-
bun run scripts/check-pro-boundary.tsexits 0, andBASELINESequal the printed counts. This change should move no count; if one moves, fix the wording rather than raising a baseline. -
bun run test:changedandquality:fixare clean, andquality:design-tokensreports no new violation.
Tests and verification
-
New
src/tests/plugins/action-slot-host.test.ts.- Render
ActionSlotHostwith a fallback built withcreateRawSnippetfromsvelte, rendering<span>core-default</span>. - Register fixture extensions through the registry's test API, and reset the registry in
afterEach.
Cover:
- With nothing registered,
core-defaultrenders. - A visible contribution renders with the given props, and
core-defaultdoes not. - With
when()false, nothing renders. - With a throwing
when(), nothing renders, and one warning is logged. - With a rejecting
load(), nothing renders, and the error is logged. - With a component that throws on render, nothing renders.
- Priority wins, then registration order.
- Flipping the
when()flag mounts, then unmounts, the outlet. - Unregistering the only contribution brings
core-defaultback.
- Render
-
src/tests/components/settings/database-settings.test.ts, as Delete the Add synced database dialog and tenant badge from Database settings #3326 rewrote it. Add two tests:- with no extension, the header row's buttons are exactly
['New', 'Open existing…']; - with the fixture contribution registered, its button renders last in the header row, with the active database id and
placementset tosettings.
- with no extension, the header row's buttons are exactly
-
Mutation checks. Run each one, confirm a test fails, then revert it:
- make the host check visibility instead of registration;
- drop the priority sort;
- pass
placement: 'menu'; - render the host before the
database.actionsoutlet.
-
Commands:
bun run --cwd packages/desktop-app test src/tests/plugins src/tests/components/settings/database-settings.test.tsbun run test:changedbun run --cwd packages/desktop-app quality:fixbun run scripts/check-pro-boundary.ts
Dependencies
- Add the Pro-boundary ratchet check and the CLAUDE.md Pro/sync placement rule #3299: the ratchet, which the baseline acceptance line relies on.
- Publish the extension host API: services, ui and testing entries, version policy #3315: the host API barrel and its snapshot test. Through Move the Pro variant machine and DATABASE_SETTINGS_NODE_ID out of the generic registry files #3309 / Adopt ADR-082's contribution contract: ids, when() predicates, keyed and isolated outlets #3310 and Add settings-section and database settings-slot extension points #3312 it also brings the registry, the outlet and the
database.actionshost. - Delete the Add synced database dialog and tenant badge from Database settings #3326: it rewrites the same header row and
database-settings.test.ts.
Risks and rollback
- The id becomes an API. Once the Pro package contributes to the slot, renaming it is a breaking (major) change. Settle the id in the ADRs first (see the pre-check).
- An empty default ships for a while. This is intended; nothing renders in core until Make the Share / Sync website action the sync-entry default; remove the Labs card #3359.
- Rollback. Revert the PR; there is no data or schema impact. If nodespace-sync already contributes to the slot, its next pin bump fails to type-check, so revert only together with the sync side.
Plan key: C9.1a (pro-boundary DAG).
🤖 Generated with Claude Code
Activity
mstomar125 commentedon Oct 1, 2026
Superseded by #3413 (umbrella against the final ADR-081–085)