Skip to content

Add the replaceable database.sync-entry action slot to the extension API #3358

Closed as not planned

Description

@mstomar125

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.

The rules for a replaceable slot (ADR-082 §2.2, §2.3, §2.4 and §8):

  1. Core renders its default only while no registered extension contributes to the slot. This depends on registration, not on visibility.

  2. 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.

  3. At most one contribution renders in each place: the visible one with the highest priority, then the first registered.

  4. The rendered contribution is keyed by <extension id>/<contribution id>, so a visibility change mounts or unmounts only that outlet.

  5. The props are { databaseId: string; placement: 'settings' | 'menu' }. Core's default ignores databaseId, because ADR-084 §1 builds the URL from constants only.

  6. 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

  1. 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.

  2. Accessors. Add them to ui-extensions.svelte.ts, reactive in the same way as the other getActive* accessors:

    • hasActionSlotContribution(slot): boolean returns true when any registered extension has a contribution for slot, whatever its when() returns.
    • getActiveActionSlotContribution(slot): { key: string; contribution: ActionSlotContribution } | null returns the visible contribution with the highest priority, with ties in registration order. It uses Turn the UI-extension registry into ADR-082's build-time frontend extension API #3317's throw-safe when() helper. key is <extension id>/<contribution id>.
  3. New host, src/lib/plugins/action-slot-host.svelte. Its props are slot, props: ActionSlotProps and fallback: Snippet.

    • With no contribution registered, it runs {@render fallback()}.
    • Otherwise it renders the active contribution, if there is one, through ExtensionOutlet with props, 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.
  4. src/lib/components/settings/sections/database-settings.svelte. In the Databases header action row, after the database.actions outlet, 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.

  5. Host API (src/lib/extension-api/index.ts, @nodespace/extension-api):

  6. 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 a database.sync-entry contribution:

    • its component renders <button>fixture-sync-entry:{databaseId}:{placement}</button>;
    • its when is driven by a $state flag the tests control.

    Keep the fixture covering every slot kind (ADR-082 §8).

Out of scope

Steps

  1. Run the startup sequence from CLAUDE.md:
    • on the primary checkout: git status, then git 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".
  2. Pre-checks:
  3. Add the types, the accessors and the host, together with src/tests/plugins/action-slot-host.test.ts.
  4. Mount the host in database-settings.svelte, and add its tests to database-settings.test.ts.
  5. Export the slot from the host API, bump the version, and update the snapshot and the fixture.
  6. 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, then ExitWorktree({action: "remove", discard_changes: true}).

Standards:

  • Use createLogger, never console.*.
  • No lint suppression, no any, no {@html}.
  • Use bun run test, never bun 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-extensions is empty.
  • A registered, visible contribution renders in the header row after the database.actions outlets, with { databaseId: <active id>, placement: 'settings' }. The default snippet does not render.
  • A registered contribution that is hidden, whose when() throws, whose load() rejects, or whose component throws leaves the place empty, and the default snippet does not render.
  • With two contributions, the higher priority wins, and a tie goes to the first registered.
  • Toggling a contribution's when() mounts and unmounts only its outlet.
  • @nodespace/extension-api exports DATABASE_SYNC_ENTRY_SLOT, ActionSlotContribution and ActionSlotProps.
  • EXTENSION_API_VERSION.minor is 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.ts exits 0, and BASELINES equal the printed counts. This change should move no count; if one moves, fix the wording rather than raising a baseline.
  • bun run test:changed and quality:fix are clean, and quality:design-tokens reports no new violation.

Tests and verification

  • New src/tests/plugins/action-slot-host.test.ts.

    • Render ActionSlotHost with a fallback built with createRawSnippet from svelte, rendering <span>core-default</span>.
    • Register fixture extensions through the registry's test API, and reset the registry in afterEach.

    Cover:

    1. With nothing registered, core-default renders.
    2. A visible contribution renders with the given props, and core-default does not.
    3. With when() false, nothing renders.
    4. With a throwing when(), nothing renders, and one warning is logged.
    5. With a rejecting load(), nothing renders, and the error is logged.
    6. With a component that throws on render, nothing renders.
    7. Priority wins, then registration order.
    8. Flipping the when() flag mounts, then unmounts, the outlet.
    9. Unregistering the only contribution brings core-default back.
  • 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 placement set to settings.
  • 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.actions outlet.
  • Commands:

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

Dependencies

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

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