Skip to content

Shell: single-product (primary-product) mode so a sole-product app owns the root surface - #65

Merged
AojdevStudio merged 2 commits into
mainfrom
feature/issue-63-primary-product-shell-mode
Jun 15, 2026
Merged

Shell: single-product (primary-product) mode so a sole-product app owns the root surface#65
AojdevStudio merged 2 commits into
mainfrom
feature/issue-63-primary-product-shell-mode

Conversation

@AojdevStudio

Copy link
Copy Markdown
Owner

Closes #63

What & why

apps/desktop was a flat walking-skeleton (App.tsx) that hardcoded the platform
panels + the Notes screen. The product-module seam presumes a multi-product shell
where each product registers under a /<namespace> segment — but a sole-product
app (OrinSync, the first real product) is the app and should own the root surface,
not sit inside generic multi-product chrome.

This adds a first-class primary-product shell mode as pure config + presentation.
No router is introduced (the seam's /<namespace> routing is still aspirational; this
issue is a boot-time layout decision, not navigation). The product-module seam contract
is untouched.

How

  • shell/config.tsShellConfig.primaryProduct, the one-line config point a
    consumer sets (shell.primaryProduct = "<namespace>"). Unset = multi-product default.
  • shell/productRegistry.tsx — additive DesktopProduct registry (frontend mirror
    of the seam; a product adds one entry, no foundation edit). Notes registered with
    its deny-by-default snapshot.
  • shell/resolveShellLayout.ts — pure, product-agnostic decision function returning
    {mode, products, primaryProduct, showProductNav, showPlatformChrome}; throws
    ShellConfigError on an unregistered namespace (fail-fast, never a silent fallback).
  • shell/Shell.tsx — thin React consumer: single-product renders only the product
    root (no nav/chrome); multi-product renders header + Products nav + platform panels +
    active product.
  • App.tsx — resolves the layout once and renders <Shell>.

The testable logic lives in the pure resolver because vitest runs in node with no DOM;
the component is a humble consumer.

Acceptance criteria

  • A config point designates one product as primary (shell/config.tsprimaryProduct).
  • With it set, the shell boots into that product's root screen with no generic product nav (mode==="single-product", showProductNav===false, showPlatformChrome===false; Shell returns just <Root />).
  • With it unset, existing multi-product behavior is unchanged (header + ping + Me + Billing + AdvancedReport + Notes all preserved, relocated intact into Shell).
  • The product-module seam contract is unchanged — added as a foundation feature, not a per-product fork (zero edits under crates/, services/, migrations/, product_module.rs).
  • A shell test covers both modes (resolveShellLayout.test.ts: multi, single, product-agnostic, and the error path).

Functional evidence

Runtime resolver demo (real execution, both modes + error):

MULTI-PRODUCT (primaryProduct unset):
  mode=multi-product showProductNav=true showPlatformChrome=true primary=null products=[notes,orinsync]
SINGLE-PRODUCT (primaryProduct="orinsync"):
  mode=single-product showProductNav=false showPlatformChrome=false primary=orinsync products=[notes,orinsync]
MISCONFIG (primaryProduct="ghost"):
  throws ShellConfigError: shell.primaryProduct "ghost" is not a registered product; registered namespaces: notes, orinsync

Gates:

  • bun run test → 6 files, 48 tests pass (43 prior + 5 new)
  • bun run typecheck / bun run lint / bun run build → clean (35 modules, 202.69 kB)
  • cargo xtask gate --scope allPASSED (fmt, clippy, workspace tests, crate-edge, frontend lint/type/build, cargo-deny, leak scan, feature-key coverage)

Browser-rendered screenshot of the shell is deferred — the Interceptor harness (Chrome+extension) wasn't running this session. The render path is otherwise covered by unit tests + the runtime demo + a render-path grep confirming every surface; re-runnable on request.

Self-review

Reviewed the full diff against the issue, the seam doc, and the project code-quality rules. App.tsx 58→19 lines; all platform surfaces relocated intact; small single-purpose modules, typed, no console, comments explain why. Stays strictly inside the slice — no router, no backend, no seam edits. No actionable findings. Cross-vendor adversarial review (Forge) folded in before merge.

Add a configurable primary-product shell mode to apps/desktop so a sole-product
app (e.g. OrinSync) owns the root surface without forking the shell.

- shell/config.ts: ShellConfig.primaryProduct — the one-line config point a
  consumer sets (unset = multi-product default).
- shell/productRegistry.tsx: additive DesktopProduct registry (the frontend
  mirror of the product-module seam; a product adds one entry, no foundation edit).
- shell/resolveShellLayout.ts: pure, product-agnostic decision fn returning the
  layout descriptor; throws ShellConfigError on an unregistered namespace
  (fail-fast, never a silent fallback).
- shell/Shell.tsx: thin React consumer — single-product renders only the product
  root (no nav/chrome); multi-product renders header + Products nav + platform
  panels + active product.
- shell/resolveShellLayout.test.ts: covers both modes + the error path (node, no DOM).
- App.tsx: resolves the layout once and renders <Shell>.

The product-module seam contract is unchanged: no edits under crates/, services/,
migrations/, or product_module.rs. Pure config + presentation.
…gle-product chrome (#63)

Address cross-vendor review (Forge) on PR #65:
- Shell now consumes ShellLayout.showPlatformChrome to gate the platform header +
  account/billing panels (was computed but never read — a dead layout flag).
- Replace the redundant unknown-namespace test with two higher-value cases: an
  empty-string primaryProduct must throw (the === undefined guard does not treat
  "" as unset), and the error names the registered namespaces.
- Document that single-product mode deliberately hides platform/billing chrome and
  does not invent billing UI; ShellLayout.showPlatformChrome is the seam for a
  follow-up platform-chrome slot.
@AojdevStudio

Copy link
Copy Markdown
Owner Author

Cross-vendor adversarial review (Forge / GPT-5.5, read-only) — resolved

Verdict was CONCERNS (no critical bugs, core decision logic correct, default multi-product path regression-free, no seam violations). Three findings, all addressed in 683336e:

  1. [major] Single-product mode hides all platform chrome, not just the nav. Acknowledged as a deliberate, now-documented decision. Issue Shell: single-product (primary-product) mode so a sole-product app owns the root surface #63 is "the product owns the root surface" and the repo rule forbids inventing billing/entitlement UI, so single-product mode renders only the product root. Made this explicit: documented in config.ts + Shell.tsx, and left ShellLayout.showPlatformChrome as the seam for a first-class platform-chrome slot (billing/account reachability) as a follow-up — captured below rather than silently expanded into this PR.
  2. [minor] showPlatformChrome was a dead field (computed, never read). FixedShell now gates the platform header + Me/Billing/AdvancedReport panels on layout.showPlatformChrome (Shell.tsx:64,87). The component renders strictly from the descriptor flags, never re-deriving from mode.
  3. [minor] The 5th test duplicated the 4th with a weaker assertion. Fixed — replaced with two higher-value cases: an empty-string primaryProduct must throw (the === undefined guard deliberately does not treat "" as unset), and the error message names the registered namespaces. Resolver tests: 5 → 6; suite 48 → 49 green.

Follow-up (not filed as an issue — flagged for the maintainer)

Platform-chrome slot for single-product mode (billing/account reachability): decide how a single-product app surfaces account/subscription affordances without forking the foundation (a kit-provided slot gated by showPlatformChrome, a composable platform-panel primitive, or product-owned). Out of scope for #63.

Gates after fixes: bun test/typecheck/lint/build green; CI gate re-running on 683336e.

@AojdevStudio
AojdevStudio merged commit 14dccb3 into main Jun 15, 2026
2 checks passed
@AojdevStudio
AojdevStudio deleted the feature/issue-63-primary-product-shell-mode branch June 15, 2026 00:48
AojdevStudio added a commit that referenced this pull request Jun 15, 2026
…#67)

Single-product mode (#63 / PR #65) hid all platform chrome, leaving a
single-product SaaS app no kit-provided way to reach account/billing.
`ShellLayout.showPlatformChrome` was the seam for it but was a dead flag
(the branch returned `<Root />` before it was read).

Make it live, opt-in (ADR-0004, issue #66 option 1 + account/billing-only):

- `ShellConfig.showPlatformChrome` (single-product only) drives the slot;
  `resolveShellLayout` reads `config.showPlatformChrome ?? false`, so the
  product still owns the root by default.
- `PlatformChromeSlot` composes the existing authority-backed `MePanel` +
  `BillingPanel` alongside the product root. It places existing panels; it
  invents no billing UI. `AdvancedReportPanel` (a product feature) is excluded.
- Multi-product mode unchanged; product-module seam untouched.

Resolver behavior is unit-tested (the kit's tested surface — vitest in node,
no DOM); `<Shell>` stays a thin consumer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell: single-product (primary-product) mode so a sole-product app owns the root surface

1 participant