Repository navigation
Description
Summary
Make core's website entry action the default content of the database.sync-entry slot from #3358, as ADR-084 §1 (proposed) specifies:
- a "Share / Sync this database…" button in Settings → Database;
- a dialog with one sentence, Continue on nodespace.ai and Cancel;
- Continue opens the constant URL
https://nodespace.ai/pro?entry=sync-databasein the system browser; - if the browser can't be opened, ADR-084's failure message, with the address selectable and a Copy link button.
In the same PR:
- delete the disabled Labs "Team synchronization" card and its
mailto:link; - replace the README's "Pro tier" section with a plain pointer to the website.
This action is the one place where core's UI mentions the paid product (ADR-081 §2b, proposed).
Context
The spec (ADR-084 §1):
- The dialog sentence says that syncing and sharing need NodeSpace Pro, which is set up on the website and replaces this app while keeping its databases.
- The URL comes only from compile-time constants.
- Core adds no database id, node id, email, person id, app version or machine identifier.
- Core makes no network request of its own (no reachability pre-check, no analytics), and does not record that the action was used.
- On an opener rejection, core shows "Couldn't open your browser. To set up sync, visit https://nodespace.ai/pro", with the address selectable and a Copy link button. It retries nothing.
- The action is shown on every platform core ships on.
- A unit test pins the exact URL string.
Opening URLs already works.
openUrl()inpackages/desktop-app/src/lib/utils/external-links.ts(lines 28-44 ataf8ddb61) uses@tauri-apps/plugin-openerunder Tauri, andwindow.open(..., 'noopener,noreferrer')in browser mode.opener:defaultinsrc-tauri/capabilities/default.jsonalready allowshttps://URLs, so no capability change is needed.
Pattern for a dialog opened from two places. stores/import-modal.svelte.ts, plus <ImportOptionsModal bind:open={importModalStore.open} /> mounted once in app-shell.svelte. Settings and the File menu both call importModalStore.show(). #3360 reuses the same approach for the menu.
Clipboard. code-block-node.svelte checks window.navigator?.clipboard before calling writeText. Follow it.
The Labs card is the <!-- Team synchronization --> comment and its <Card> in labs-settings.svelte (lines 53-75 at af8ddb61). labs-settings.test.ts covers it in:
- the doc comment (1-10);
- the
syncEnabledresets (22, 29); - the three-entry test (33-43);
- five tests (105-162).
The README section is "### Pro tier" in README.md (lines 47-49). The Discord links on line 7 and under "Community" stay.
Ratchet. #3302 counts these markers on the lines this PR deletes:
labs-settings.svelte62-63: cloudBindState ×2;labs-settings.test.ts: cloudBindState ×6, proWording ×1, proSyncModule ×1;- README 47-49: proWording ×2, editionBranching ×1, tenantWording ×1.
The new explanation sentence adds one proWording line. That is the entry-point copy ADR-081 §2b allows, and the net count still goes down. Use the CLI's printed numbers, not these.
Scope
-
New
packages/desktop-app/src/lib/services/sync-entry.ts. Its module doc cites ADR-081 §2b and ADR-084 §1, and says the URL must stay built from constants.export const SYNC_ENTRY_URL = 'https://nodespace.ai/pro?entry=sync-database'; export const SYNC_ENTRY_FAILURE_ADDRESS = 'https://nodespace.ai/pro'; export const SYNC_ENTRY_LABEL = 'Share / Sync this database…'; export const SYNC_ENTRY_TITLE = 'Share / Sync this database'; export const SYNC_ENTRY_EXPLANATION = '<one sentence, ADR-084 §1; see below>'; export const SYNC_ENTRY_CONTINUE_LABEL = 'Continue on nodespace.ai'; export const SYNC_ENTRY_FAILURE_TEXT = "Couldn't open your browser. To set up sync, visit"; export async function openSyncEntryUrl(): Promise<boolean>;
- The URL. Take its form from ADR-084 decision 1 as accepted. The recommended option, (b), is the URL above. Under option (a), use
https://nodespace.ai/prowith no parameter. - The explanation. Unless ADR-084 as merged gives other text, use: "Syncing and sharing need NodeSpace Pro, which you set up on nodespace.ai and which replaces this app while keeping your databases." Keep it on one line: it is the one sanctioned "Pro" line in core's UI.
openSyncEntryUrl()takes no arguments, so nothing can be appended to the URL.- It awaits
openUrl(SYNC_ENTRY_URL)and returnstrue. - On a rejection, it calls
log.warn('Could not open the browser for the sync entry', err)throughcreateLogger('SyncEntry')and returnsfalse. - It never throws, logs nothing on success, and writes no state.
- It awaits
- The URL. Take its form from ADR-084 decision 1 as accepted. The recommended option, (b), is the URL above. Under option (a), use
-
New
packages/desktop-app/src/lib/stores/sync-entry-dialog.svelte.ts, followingimport-modal.svelte.ts(an ADR-049 rune store):open = $state(false),failed = $state(false)andcopied = $state(false);show()resetsfailedandcopied, then setsopen = true;- export the singleton as
syncEntryDialog.
-
New
packages/desktop-app/src/lib/components/settings/sync-entry-dialog.svelte. Build it with theDialog.*andButtoncomponents, like the Remove dialog indatabase-settings.svelte. It takes a bindableopenprop.Dialog.TitleshowsSYNC_ENTRY_TITLE, andDialog.DescriptionshowsSYNC_ENTRY_EXPLANATION.- While
failedis set, show arole="alert"block, styled like thedatabaseStore.erroralert indatabase-settings.svelte. It contains:SYNC_ENTRY_FAILURE_TEXT;SYNC_ENTRY_FAILURE_ADDRESSin a<span class="select-all font-mono">;- a Copy link button. It writes the address with
navigator.clipboard.writeText, guarded as incode-block-node.svelte, and then shows "Copied". If the write fails, it logs a warning and the address stays selectable.
Dialog.Footerhas two buttons:- Cancel (
variant="ghost") closes the dialog. - Continue on nodespace.ai (the default variant) awaits
openSyncEntryUrl(). Ontrueit closes the dialog; onfalseit setsfailed. Nothing retries automatically, and Continue stays usable.
- Cancel (
- Use design tokens only, and no
{@html}.
-
packages/desktop-app/src/lib/components/layout/app-shell.svelte: mount<SyncEntryDialog bind:open={syncEntryDialog.open} />once, next to<ImportOptionsModal ...>. Add a one-line comment that the Settings action and the File menu share this instance. -
New
packages/desktop-app/src/lib/components/settings/sync-entry-action.svelte:<Button variant="outline" size="sm" onclick={() => syncEntryDialog.show()}>{SYNC_ENTRY_LABEL}</Button>. It takes the slot props and ignores them. -
database-settings.svelte: thesyncEntryDefaultsnippet from Add the replaceable database.sync-entry action slot to the extension API #3358 renders<SyncEntryAction />. -
packages/desktop-app/src/lib/components/settings/sections/labs-settings.svelte: delete the<!-- Team synchronization -->comment and its<Card>. Keep thelabsFlagsimport, which AI Chat and Playbooks still use. -
README.md: replace "### Pro tier" and its paragraph with:Sync and sharing
NodeSpace keeps your data on your machine. To sync a database across your devices or share it with your team, choose Share / Sync this database… in Settings → Database, or visit nodespace.ai/pro. You start a trial or a plan there and download the edition with sync, which replaces this app and keeps your local databases.
Don't mention
nodespaced-pro, "the same desktop app", tenants, Discord-for-access or "end-to-end encrypted". Keep the Discord links on line 7 and under "Community".
Out of scope
- The native File menu entry (Add a File menu item that opens the Share / Sync this database entry #3360). It will call
syncEntryDialog.show(). - Deleting
labsFlags.syncEnabledand its store tests (Delete the Labs syncEnabled flag and guard the free app's single sync entry point #3361). This PR only stopslabs-settings.sveltefrom reading it. - The Pro package's contribution (nodespace-sync) and the landing page (nodespace-website).
- "Share this node".
Steps
- Run the startup sequence from CLAUDE.md, with
EnterWorktree({name: "issue-<N>-sync-entry-action"}):bun install;bun run testfor a baseline;bun run gh:comment <N> "Frontend: X passed",bun run gh:assign <N> "@me",bun run gh:status <N> "In Progress".
- Pre-checks. If any fails, stop and report it:
- ADR-084 is merged; check nodespace-docs
origin/main. Copy its URL (decision 1), dialog sentence and failure text verbatim. - Reconnect nodespace-website Cloudflare Pages Git integration (human step) #3286 is closed, and a person has opened
https://nodespace.ai/pro?entry=sync-databasein a browser and confirmed it shows the dedicated Share / Sync page, not the home page. Acurlthat returns 200 proves nothing: the site answers every path with its home page. - The nodespace-sync contribution to
database.sync-entryhas been reviewed against a core revision that includes Add the replaceable database.sync-entry action slot to the extension API #3358. - Move the Pro sync and collaboration UI out of core #3333 is merged:
git grep -n "pro-plugin" -- packages/desktop-app/srcreturns nothing.
- ADR-084 is merged; check nodespace-docs
- Write
sync-entry.ts, the store, the dialog and the action, with their tests. - Mount the dialog in
app-shell.svelte, and fill the slot default indatabase-settings.svelte. - Delete the Labs card and update
labs-settings.test.ts. - Rewrite the README section.
- Run
bun run scripts/check-pro-boundary.tsand paste the printedBASELINES. - Finish:
bun run test:changed;bun run --cwd packages/desktop-app quality:fix, then commit;git push origin HEAD:issue-<N>-sync-entry-action;gh pr create --repo NodeSpaceAI/nodespace-core --base main --head issue-<N>-sync-entry-action. In the PR description:- name the explanation line as the ADR-081 §2b entry-point copy;
- ask nodespace-sync to enable its contribution in the same change that bumps its pin past this PR.
- 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 and
$lib/utils/external-linkswithvi.mock. - Code comments cite ADR-081 and ADR-084, never issue numbers.
- No Pro, tenant, Supabase or nodespace-sync wording in comments. The explanation constant is the only "Pro" line.
Acceptance criteria
- When no extension contributes to
database.sync-entry, Settings → Database shows "Share / Sync this database…" as the last item of the Databases header row. - Clicking it opens a dialog with the ADR-084 sentence, Continue on nodespace.ai and Cancel. Cancel closes the dialog with no side effect.
- Continue calls
openUrlexactly once, withhttps://nodespace.ai/pro?entry=sync-database. The URL has one query parameter,entry, and no version or identifier. - Nothing in the flow calls
fetch,invokeorlocalStorage, and nothing is logged on success. - If
openUrlrejects, the dialog shows the failure text andhttps://nodespace.ai/proin a selectable element, and Copy link writes that address to the clipboard. Nothing throws, and nothing retries. - With a fixture contribution registered to the slot, core's button does not render. This checks Add the replaceable database.sync-entry action slot to the extension API #3358's rule end to end.
- The dialog is mounted once, in
app-shell.svelte. - Labs renders exactly two cards, AI Chat and Playbooks.
git grep -n "developer@nodespace.ai\|Team synchronization" -- packages/desktop-app/srcreturns nothing. - The README section is replaced as described.
git grep -n "nodespaced-pro\|Pro tier" -- README.mdreturns nothing, and the Discord community links remain. -
bun run scripts/check-pro-boundary.tsexits 0, andBASELINESare lowered to the printed counts. -
bun run test:changedandquality:fixare clean, andquality:design-tokensreports no new violation.
Tests and verification
- New
src/tests/services/sync-entry.test.ts. Mock$lib/utils/external-linksand the logger. Cover:SYNC_ENTRY_URLequals the string literal'https://nodespace.ai/pro?entry=sync-database', and[...new URL(SYNC_ENTRY_URL).searchParams.keys()]equals['entry'];SYNC_ENTRY_FAILURE_ADDRESSequals the literal'https://nodespace.ai/pro';openSyncEntryUrl()callsopenUrlonce with the literal and resolvestrue;- a rejected
openUrlresolvesfalse, logs one warning, and doesn't throw; openSyncEntryUrl.length === 0.
- New
src/tests/components/settings/sync-entry-dialog.test.ts. Spy onglobalThis.fetch, and stubnavigator.clipboardwithObject.defineProperty. Cover:- when open, the title, the explanation constant and both buttons render;
- Cancel closes the dialog, and
openUrlis not called; - Continue calls
openUrlwith the literal and closes the dialog; - a rejection renders the
role="alert"block, with the address in a.select-allelement; - Copy link calls
writeText('https://nodespace.ai/pro')and shows "Copied"; - a missing clipboard API doesn't throw;
fetchis never called.
src/tests/components/settings/database-settings.test.ts:- the header row's buttons are
['New', 'Open existing…', 'Share / Sync this database…']; - clicking the last one sets
syncEntryDialog.open; - with the fixture contribution registered, that button is absent.
- the header row's buttons are
src/tests/components/settings/labs-settings.test.ts:- rewrite the doc comment (1-10) to describe two entries, with no Pro or
proSyncwording; - drop the
labsFlags.syncEnabled = falseresets (22, 29); - turn 33-43 into "renders exactly two entries: AI Chat, Playbooks" (
toHaveLength(2)); - delete the five Team synchronization tests (105-162);
- add "has no mailto link":
container.querySelector('a[href^="mailto:"]')is null.
- rewrite the doc comment (1-10) to describe two entries, with no Pro or
- Mutation checks. Each must fail a test; revert it afterwards:
- change one character of
SYNC_ENTRY_URL; - append
&v=1to it; - make
openSyncEntryUrlrethrow; - drop the
failedbranch; - make Copy link copy
SYNC_ENTRY_URL; - leave the Labs card in.
- change one character of
- Commands:
bun run --cwd packages/desktop-app test src/tests/services/sync-entry.test.ts src/tests/components/settingsbun run test:changedbun run --cwd packages/desktop-app quality:fixbun run scripts/check-pro-boundary.ts
- Manual check. Run
bun run tauri:devagainst an isolatedNODESPACE_HOME, never the live~/.nodespace:- Open Settings → Database, click the action, then click Continue. The default browser opens the entry URL.
- Cancel does nothing.
- Settings → Labs shows two cards.
- In
bun run dev:browser, Continue opens a new tab.
Dependencies
- Add the replaceable database.sync-entry action slot to the extension API #3358: the slot, its host in the header row, and the empty default this issue fills.
- Move the Pro sync and collaboration UI out of core #3333:
- the Pro UI has left core, and the Pro package registers through
virtual:nodespace-extensions, so its contribution comes from nodespace-sync rather than from core'spro-plugin.ts; - Move the Pro sync and collaboration UI out of core #3333 also edits
app-shell.svelte.
- the Pro UI has left core, and the Pro package registers through
- NodeSpaceAI/nodespace-docs#94 (ADR-084) (ADR-084): the URL (decision 1), the sentence, the dialog and the failure text.
- Reconnect nodespace-website Cloudflare Pages Git integration (human step) #3286: the website deploys again.
- WEB: the dedicated landing page serves the entry URL.
- Pin gate: the Pro GUI and the Pro daemon build from one pinned core revision, so a Pro build picks up this change only when nodespace-sync bumps its pin.
- SYNC: the Pro package's contribution to the slot has been reviewed against a core revision that includes Add the replaceable database.sync-entry action slot to the extension API #3358 (ADR-081 §7 step 7).
Risks and rollback
- A paying user sees the purchase action if nodespace-sync bumps its pin past this PR without enabling its contribution. The SYNC gate and the note in the PR description cover this.
- Users land on a 404 or the home page if the site isn't ready. The WEB gate and the human pre-check cover this.
- Rollback. Revert the PR. This restores the Labs card and the old README, with no data or schema impact. The slot from Add the replaceable database.sync-entry action slot to the extension API #3358 stays.
Plan key: C9.1b (pro-boundary DAG).
🤖 Generated with Claude Code
Activity
mstomar125 commentedon Oct 1, 2026
Superseded by #3413 (umbrella against the final ADR-081–085)