@cosmoops/connect-sdk 0.9.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +566 -0
  2. package/README.md +113 -42
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +20 -1
  6. package/dist/cjs/client/components/ai-screens.js +73 -0
  7. package/dist/cjs/client/components/ai-widgets.js +37 -0
  8. package/dist/cjs/client/components/app-shell.js +75 -41
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +50 -5
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +41 -0
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +49 -23
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +96 -37
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/index.js +72 -2
  29. package/dist/cjs/client/provider.js +57 -2
  30. package/dist/cjs/client/use-checkout.js +9 -5
  31. package/dist/cjs/errors.js +1 -1
  32. package/dist/cjs/index.js +16 -1
  33. package/dist/cjs/mcp/index.js +90 -0
  34. package/dist/cjs/mcp/tools.js +369 -0
  35. package/dist/cjs/next/index.js +2 -2
  36. package/dist/cjs/server/client.js +25 -2
  37. package/dist/cjs/server/handler.js +203 -18
  38. package/dist/cjs/server/http.js +6 -2
  39. package/dist/cjs/server/resources/acting-as.js +16 -0
  40. package/dist/cjs/server/resources/ai.js +22 -2
  41. package/dist/cjs/server/resources/data.js +374 -0
  42. package/dist/cjs/server/resources/identity.js +7 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store-schema.js +66 -0
  48. package/dist/cjs/store.js +152 -0
  49. package/dist/cjs/ui/empty-state.js +28 -0
  50. package/dist/cjs/ui/index.js +44 -0
  51. package/dist/cjs/ui/page-header.js +27 -0
  52. package/dist/cjs/ui/page-shell.js +44 -0
  53. package/dist/cjs/ui/section.js +20 -0
  54. package/dist/cjs/ui/setting-row.js +14 -0
  55. package/dist/cjs/ui/status-chip.js +17 -0
  56. package/dist/esm/cli/doctor.js +3 -3
  57. package/dist/esm/cli/init.js +3 -3
  58. package/dist/esm/client/components/access-gate.js +20 -1
  59. package/dist/esm/client/components/ai-screens.js +69 -0
  60. package/dist/esm/client/components/ai-widgets.js +37 -1
  61. package/dist/esm/client/components/app-shell.js +81 -47
  62. package/dist/esm/client/components/apps-grid.js +3 -2
  63. package/dist/esm/client/components/ask-ai.js +51 -6
  64. package/dist/esm/client/components/confirm-dialog.js +37 -0
  65. package/dist/esm/client/components/cursor-pagination.js +19 -0
  66. package/dist/esm/client/components/filter-tabs.js +21 -0
  67. package/dist/esm/client/components/grievance-notice.js +27 -0
  68. package/dist/esm/client/components/link-button.js +17 -10
  69. package/dist/esm/client/components/logo.js +4 -3
  70. package/dist/esm/client/components/nav.js +40 -0
  71. package/dist/esm/client/components/plan-pricing.js +25 -0
  72. package/dist/esm/client/components/plans.js +2 -2
  73. package/dist/esm/client/components/platform-gate.js +45 -0
  74. package/dist/esm/client/components/shell-context.js +141 -0
  75. package/dist/esm/client/components/side-nav.js +48 -21
  76. package/dist/esm/client/components/sidebar.js +120 -0
  77. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  78. package/dist/esm/client/components/subscription.js +2 -2
  79. package/dist/esm/client/components/user-menu.js +95 -38
  80. package/dist/esm/client/components/workspace-switcher.js +97 -0
  81. package/dist/esm/client/index.js +47 -4
  82. package/dist/esm/client/provider.js +53 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +4 -0
  86. package/dist/esm/mcp/index.js +85 -0
  87. package/dist/esm/mcp/tools.js +366 -0
  88. package/dist/esm/next/index.js +2 -2
  89. package/dist/esm/server/client.js +25 -2
  90. package/dist/esm/server/handler.js +203 -18
  91. package/dist/esm/server/http.js +6 -2
  92. package/dist/esm/server/resources/acting-as.js +13 -0
  93. package/dist/esm/server/resources/ai.js +22 -2
  94. package/dist/esm/server/resources/data.js +369 -0
  95. package/dist/esm/server/resources/identity.js +7 -3
  96. package/dist/esm/server/resources/kv.js +80 -0
  97. package/dist/esm/server/resources/platform.js +39 -0
  98. package/dist/esm/server/resources/sessions.js +1 -1
  99. package/dist/esm/server/resources/workspaces.js +70 -0
  100. package/dist/esm/store-schema.js +62 -0
  101. package/dist/esm/store.js +141 -0
  102. package/dist/esm/ui/empty-state.js +25 -0
  103. package/dist/esm/ui/index.js +34 -0
  104. package/dist/esm/ui/page-header.js +24 -0
  105. package/dist/esm/ui/page-shell.js +41 -0
  106. package/dist/esm/ui/section.js +17 -0
  107. package/dist/esm/ui/setting-row.js +11 -0
  108. package/dist/esm/ui/status-chip.js +14 -0
  109. package/dist/types/client/components/ai-screens.d.ts +25 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +34 -1
  111. package/dist/types/client/components/app-shell.d.ts +65 -27
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +47 -1
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +24 -0
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +27 -13
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +68 -22
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/index.d.ts +29 -5
  129. package/dist/types/client/provider.d.ts +109 -6
  130. package/dist/types/client/use-checkout.d.ts +8 -4
  131. package/dist/types/errors.d.ts +1 -1
  132. package/dist/types/index.d.ts +6 -0
  133. package/dist/types/mcp/index.d.ts +74 -0
  134. package/dist/types/mcp/tools.d.ts +107 -0
  135. package/dist/types/next/index.d.ts +1 -1
  136. package/dist/types/server/client.d.ts +25 -2
  137. package/dist/types/server/http.d.ts +8 -2
  138. package/dist/types/server/index.d.ts +4 -0
  139. package/dist/types/server/resources/acting-as.d.ts +11 -0
  140. package/dist/types/server/resources/ai.d.ts +20 -2
  141. package/dist/types/server/resources/data.d.ts +258 -0
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store-schema.d.ts +52 -0
  146. package/dist/types/store.d.ts +96 -0
  147. package/dist/types/types.d.ts +369 -16
  148. package/dist/types/ui/empty-state.d.ts +34 -0
  149. package/dist/types/ui/index.d.ts +40 -0
  150. package/dist/types/ui/page-header.d.ts +34 -0
  151. package/dist/types/ui/page-shell.d.ts +39 -0
  152. package/dist/types/ui/section.d.ts +26 -0
  153. package/dist/types/ui/setting-row.d.ts +18 -0
  154. package/dist/types/ui/status-chip.d.ts +36 -0
  155. package/package.json +15 -2
package/README.md CHANGED
@@ -12,13 +12,14 @@ npm install @cosmoops/connect-sdk
12
12
  npm install streamdown # optional — Ask AI's markdown renderer, see step 4
13
13
  ```
14
14
 
15
- Two entry points, and the split is a security boundary, not a preference:
15
+ Five entry points, and the split is a security boundary, not a preference:
16
16
 
17
17
  | Import | Runs in | Holds the API key |
18
18
  | --- | --- | --- |
19
19
  | `@cosmoops/connect-sdk/server` | your backend | **yes** |
20
20
  | `@cosmoops/connect-sdk/client` | the browser | never |
21
21
  | `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
22
+ | `@cosmoops/connect-sdk/mcp` | your backend | **yes** (store, as MCP tools) |
22
23
  | `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
23
24
 
24
25
  The browser half never sees a credential. It calls your own origin, and
@@ -35,7 +36,7 @@ The fast path — most integrations are this and nothing else. Not on Next.js?
35
36
  Skip to [Other frameworks](#other-frameworks); the shape is the same, you
36
37
  just wire the session cookie yourself.
37
38
 
38
- **1. Install and get credentials.** From your application's Integration tab
39
+ **1. Install and get credentials.** From your application's Connect client section
39
40
  in Connect, copy the API key and OAuth client id.
40
41
 
41
42
  ```bash
@@ -87,7 +88,7 @@ import "streamdown/styles.css";
87
88
  Skip the last two if you pass `askAi={false}` to `AppShell`. The colour tokens
88
89
  Streamdown expects are already mapped onto HeroUI's in `theme.css`.
89
90
 
90
- **5. Register the redirect URI.** In the application's Integration tab, add
91
+ **5. Register the redirect URI.** In the application's Connect client section, add
91
92
  `{your origin}/api/connect/callback`. This is the one step the SDK cannot do
92
93
  for you.
93
94
 
@@ -123,7 +124,7 @@ layout and Connect's dashboard rather than files it can safely write. See
123
124
 
124
125
  Two things most integrations add next:
125
126
 
126
- - **A shell** — `AppShell` from `/client` is the sidebar, top bar and access
127
+ - **A shell** — `AppShell` from `/client` is the rail and the access
127
128
  gate every application platform needs. See [App shell](#app-shell).
128
129
  - **Route protection before render** — see [Route protection and
129
130
  `proxy.ts`](#route-protection-and-proxyts) below; `requireConnectSession()`
@@ -136,7 +137,7 @@ for you under the hood.
136
137
 
137
138
  ## Server setup
138
139
 
139
- One credential does three jobs — the API key plaintext from your application's Integration tab is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
140
+ One credential does three jobs — the API key plaintext from your application's Connect client section is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
140
141
 
141
142
  ```ts
142
143
  import { ConnectClient } from "@cosmoops/connect-sdk/server";
@@ -518,17 +519,17 @@ that URL yourself instead.
518
519
  import {
519
520
  AppShell,
520
521
  ConnectLoginButton, ConnectRegisterButton,
521
- ConnectUserMenu, ConnectAppsGrid, ConnectPlans,
522
+ ConnectUserButton, ConnectAppsGrid, ConnectPlans,
522
523
  } from "@cosmoops/connect-sdk/client";
523
524
 
524
- // The whole frame — sidebar, top bar, access gate, Ask AI. See "App shell" below.
525
- <AppShell name="Dispatch" nav={nav}>{children}</AppShell>
525
+ // The whole frame — the rail, the access gate, Ask AI. See "App shell" below.
526
+ <AppShell name="Dispatch" nav={nav} secondaryNav={settingsNav}>{children}</AppShell>
526
527
 
527
528
  // Or the pieces, if you already have a layout. The pair swaps automatically
528
529
  // on sign-in state.
529
- <ConnectAppsGrid />
530
+ <ConnectAppsGrid variant="icon" />
530
531
  <ConnectLoginButton />
531
- <ConnectUserMenu />
532
+ <ConnectUserButton variant="inline" />
532
533
 
533
534
  // Pricing page
534
535
  <ConnectPlans />
@@ -536,10 +537,13 @@ import {
536
537
 
537
538
  | Component | What it does |
538
539
  | --- | --- |
539
- | `AppShell` | **The whole application frame** — collapsible sidebar, top bar with the launcher and account menu, and your pages in the middle. Mounts `ConnectAccessGate` for you. |
540
- | `ConnectSideNav` | The sidebar's nav column on its own — sections, one level of children, active-route highlight — for an app that already has its own chrome. |
540
+ | `AppShell` | **The whole application frame** — one collapsible rail carrying the brand, workspace switcher, both navs, the launcher and the account row, with your pages beside it. No top bar. Mounts `ConnectAccessGate` for you. The one `/client` export without the `Connect` prefix; alias it on import if your own layout shares the name. |
541
+ | `ConnectSidebar` · `ConnectSidebarHeader` · `ConnectSidebarBody` · `ConnectSidebarFooter` · `ConnectSidebarRow` | The rail in pieces, for chrome the props cannot express. `AppShell` composes exactly these; they coordinate through `ConnectShellProvider`. |
542
+ | `ConnectSidebarNav` | The rail's nav column on its own — sections, one level of children, active-route highlight — for an app that already has its own chrome. (Was `ConnectSideNav`.) |
541
543
  | `ConnectLoginButton` · `ConnectRegisterButton` | "Continue with Connect", in the shape people know from Google. Render nothing when signed in. |
542
- | `ConnectUserMenu` | Avatar, name, workspace. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. |
544
+ | `ConnectUserButton` | The account row at the foot of the rail — circular avatar, name over email — opening a modal with identity, sessions and the Connect destinations. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. (Was `ConnectUserMenu`.) |
545
+ | `ConnectProfileModal` | The same modal with no trigger, driven by `isOpen`/`onOpenChange` — for an app keeping its own account control. |
546
+ | `ConnectWorkspaceSwitcher` | Sits in the rail's header. Hides itself for an account with one workspace; shows unusable ones disabled, with the reason. |
543
547
  | `ConnectAppsGrid` | Google-style launcher — grid icon opening a modal of every app this workspace can open. |
544
548
  | `ConnectPlans` | Monthly/annual toggle, features, add-ons, live discount, coupon field, and the subscribe / upgrade / downgrade / cancel flow. |
545
549
  | `ConnectPlansPage` | A whole `/plans` page — readiness banner, current subscription, and the grid. What `connect-sdk init` scaffolds. |
@@ -706,21 +710,20 @@ export function Shell({ children }: { children: React.ReactNode }) {
706
710
  nav={[
707
711
  { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
708
712
  { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
713
+ ]}
714
+ // Pinned to the foot of the rail: the routes you go to in order to
715
+ // change how the job works, not to do it.
716
+ secondaryNav={[
709
717
  {
710
- title: "Workspace",
718
+ label: "Settings",
719
+ href: "/settings",
720
+ icon: <Settings className="size-5" />,
711
721
  items: [
712
- {
713
- label: "Settings",
714
- href: "/settings",
715
- icon: <Settings className="size-5" />,
716
- items: [
717
- { label: "Members", href: "/settings/members" },
718
- { label: "Billing", href: "/settings/billing" },
719
- ],
720
- },
721
- { label: "Plans", href: "/plans", icon: <Receipt className="size-5" />, gateExempt: true },
722
+ { label: "Members", href: "/settings/members" },
723
+ { label: "Billing", href: "/settings/billing" },
722
724
  ],
723
725
  },
726
+ { label: "Plans", href: "/plans", icon: <Receipt className="size-5" />, gateExempt: true },
724
727
  ]}
725
728
  >
726
729
  {children}
@@ -729,10 +732,16 @@ export function Shell({ children }: { children: React.ReactNode }) {
729
732
  }
730
733
  ```
731
734
 
732
- That is the entire layout of a application platform. Everything that should look
733
- identical across the family — the lockup, the rail geometry, the active-route
734
- treatment, the app launcher, the account menu, the mobile drawer — comes from
735
- here; only `name`, `icon` and `nav` differ per platform.
735
+ That is the entire layout of an application platform. Everything that should
736
+ look identical across the family — the lockup, the rail geometry, the
737
+ active-route treatment, the app launcher, the account row, the mobile drawer —
738
+ comes from here; only `name`, `icon` and the two navs differ per platform.
739
+
740
+ **There is no top bar.** The launcher and the account row live at the foot of
741
+ the rail, one press from wherever the pointer already is, and a page's own
742
+ heading belongs to the page — put it in `ConnectPageShell` from
743
+ `@cosmoops/connect-sdk/ui`. Below `md` a slim bar carries the drawer trigger
744
+ and nothing else.
736
745
 
737
746
  **Routing is a prop.** The SDK ships into any React app and cannot import
738
747
  `next/link`, so `renderLink` is how your router gets involved; the default is a
@@ -747,7 +756,7 @@ around `children` — an app that ships a shell and forgets the gate is an app
747
756
  that lets a suspended workspace keep working. The chrome is never gated: the
748
757
  sidebar, the lockup, the account menu and sign-out stay reachable in every
749
758
  blocked state. Routes that resolve a block rather than needing one — your
750
- pricing page — are flagged where they are declared:
759
+ pricing page — are flagged where they are declared, in either nav:
751
760
 
752
761
  ```tsx
753
762
  { label: "Plans", href: "/plans", gateExempt: true }
@@ -771,26 +780,38 @@ Connect, and `ai:chat` on the API key behind your bridge.
771
780
 
772
781
  Other things worth knowing:
773
782
 
774
- - **The rail collapses to icons** and remembers the choice per browser
775
- (`localStorage`). Drive it yourself with `collapsed` + `onCollapsedChange`,
776
- or pin it open with `collapsible={false}`.
777
- - **Below `md` there is no rail**, only a drawer behind the top bar's menu
783
+ - **The rail starts collapsed** and remembers the choice per browser
784
+ (`localStorage`). The icon rail is the resting state; expanding is the
785
+ deliberate act. Expanded, the collapse control sits at the header's trailing
786
+ edge; collapsed, hovering the lockup swaps it for the expand glyph — so the
787
+ rail spends no row on a permanent control, at the cost of the lockup no
788
+ longer being a link home while collapsed. Pass `defaultCollapsed={false}` to
789
+ open by default, drive it with `collapsed` + `onCollapsedChange`, or pin it
790
+ open with `collapsible={false}`.
791
+ - **The collapse animates width and nothing else.** Every row is laid out so
792
+ its glyph sits on the same horizontal centre in both states, and labels are
793
+ clipped rather than re-measured — so no row reflows and nothing drifts
794
+ sideways mid-transition.
795
+ - **Below `md` there is no rail**, only a drawer behind the mobile bar's menu
778
796
  button — a 64px icon strip on a phone is a worse drawer, not a smaller one.
779
797
  - **Children unfold only on the branch you are on.** A sidebar that opens
780
798
  every branch at once is a sitemap.
781
799
  - **No `isSignedIn` branch, in here or in yours.** The auth buttons render
782
- nothing once a session exists, the account menu renders nothing without one,
800
+ nothing once a session exists, the account row renders nothing without one,
783
801
  and the launcher renders nothing when signed out.
784
- - `title`, `headerActions`, `sidebarFooter` and `footer` are slots;
785
- `contentClassName` replaces the content pane's default max-width and
786
- padding.
787
- - `theme` / `onThemeChange` pass straight through to `ConnectUserMenu` — the
802
+ - `secondaryNav`, `sidebarFooter` and `footer` are slots; `contentClassName`
803
+ replaces the content pane's default max-width and padding. (`title` and
804
+ `headerActions` are gone with the top bar — see the 0.12.0 changelog entry.)
805
+ - `theme` / `onThemeChange` pass straight through to `ConnectUserButton` — the
788
806
  SDK has no theme provider of its own, deliberately.
789
807
 
790
- `ConnectSideNav` is the nav column alone, for an app with chrome it already
791
- likes. `resolveActivePath` and `isNavItemActive` are the same pure functions
792
- the rail highlights with, exported so a breadcrumb or a document title derived
793
- from your nav cannot disagree with it.
808
+ `ConnectSidebarNav` is the nav column alone, for an app with chrome it already
809
+ likes, and `ConnectSidebar` / `ConnectSidebarHeader` / `ConnectSidebarBody` /
810
+ `ConnectSidebarFooter` / `ConnectSidebarRow` are the rest of the rail — the
811
+ exact pieces `AppShell` composes, so chrome the props cannot express is a
812
+ rearrangement rather than a fork. `resolveActivePath` and `isNavItemActive` are
813
+ the same pure functions the rail highlights with, exported so a breadcrumb or a
814
+ document title derived from your nav cannot disagree with it.
794
815
 
795
816
  ### Logo
796
817
 
@@ -825,6 +846,56 @@ So the hand-off is a URL, and Connect renders it as a modal over the relevant pa
825
846
 
826
847
  Full history: [CHANGELOG.md](./CHANGELOG.md). Latest first:
827
848
 
849
+ ### 0.12.0 — breaking
850
+
851
+ The application shell is one column. **The top bar is gone**; the rail holds
852
+ everything it used to, and starts collapsed.
853
+
854
+ - **`title` and `headerActions` are removed** with the bar that held them. A
855
+ page's heading goes in `ConnectPageShell` (from `/ui`), next to that page's
856
+ own breadcrumbs and actions; an application-level control goes in
857
+ `sidebarFooter` as a `ConnectSidebarRow`.
858
+ - **`defaultCollapsed` now defaults to `true`.** Pass `false` for 0.11's
859
+ behaviour.
860
+ - **`ConnectSideNav` → `ConnectSidebarNav`**, **`ConnectUserMenu` →
861
+ `ConnectUserButton`**. Both old names remain as deprecated aliases.
862
+ `AppShell` keeps its name — the one export here without the `Connect`
863
+ prefix, as decided in 0.8.0.
864
+ - **New:** `secondaryNav` (a second nav group pinned to the foot of the rail),
865
+ `ConnectProfileModal` (the account modal with no trigger), and the rail's
866
+ own pieces — `ConnectSidebar`, `ConnectSidebarHeader`, `ConnectSidebarBody`,
867
+ `ConnectSidebarFooter`, `ConnectSidebarRow`, `ConnectShellProvider`,
868
+ `useConnectShell`, `sidebarRowClass`.
869
+ - **The workspace switcher moved into the rail's header** and is collapse-aware;
870
+ the account row sits at the foot with a circular avatar.
871
+ - **Fixed:** the closed Ask AI panel occupied 400px at `lg` and up on every page
872
+ with `askAi` on; sidebar contrast (the active row was **below WCAG AA** in
873
+ both schemes); the launcher and account rows looked permanently selected.
874
+
875
+ ### 0.11.0 — breaking
876
+
877
+ - **`ConnectUser.workspaceId`, `workspaceName` and `workspaceType` are
878
+ nullable.** They are `null` for exactly one thing: a **Connect platform
879
+ staff** session, which belongs to no workspace. If you never enable staff
880
+ access — it is off by default — this is a type narrow and nothing else.
881
+ `?? ""` is the wrong fix: decide what your tenanting does with a session that
882
+ has no tenant.
883
+ - **Connect platform staff sign-in**, `platformAccess` on `exchangeCode()` and
884
+ `getUser()`, `<ConnectPlatformGate>` and `useConnectPlatformAccess()`.
885
+
886
+ ### 0.10.0
887
+
888
+ - **`@cosmoops/connect-sdk/ui`** — `ConnectPageShell`, `ConnectSection`,
889
+ `ConnectEmptyState`, `ConnectStatusChip`, `ConnectSettingRow`. Deliberately
890
+ *not* a `"use client"` module, so a page shell does not become a client
891
+ boundary.
892
+ - `ConnectLinkButton`, `ConnectConfirmDialog`, `ConnectCursorPagination`,
893
+ `ConnectFilterTabs`, `ConnectAccountSessions` — built since 0.8 and reachable
894
+ from no entry point until now.
895
+ - `ConnectWorkspaceSwitcher`, `useConnectWorkspaces()`,
896
+ `useConnectWorkspaceMembers()`, and the `workspaces` resource on the server
897
+ client.
898
+
828
899
  ### 0.9.0
829
900
 
830
901
  - **Ask AI**, mounted by `AppShell` by default — a chat panel grounded in the
@@ -162,8 +162,8 @@ function checkEnv(root) {
162
162
  status: "fail",
163
163
  title: `${name} set`,
164
164
  detail: declaredEmpty
165
- ? `Declared but empty in .env.local. The value is in your application's Integration tab in Connect.`
166
- : `Not found in .env.local or the environment. Both values are in your application's Integration tab in Connect.`,
165
+ ? `Declared but empty in .env.local. The value is in your application's Connect client section in Connect.`
166
+ : `Not found in .env.local or the environment. Both values are in your application's Connect client section in Connect.`,
167
167
  };
168
168
  });
169
169
  }
@@ -176,7 +176,7 @@ function checkRedirectUri() {
176
176
  return {
177
177
  status: "warn",
178
178
  title: "Redirect URI registered in Connect",
179
- detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's Integration tab - including the deployed origin, not just http://localhost:3000.",
179
+ detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's Connect client section - including the deployed origin, not just http://localhost:3000.",
180
180
  };
181
181
  }
182
182
  const MARKER = { pass: " ok ", warn: " note ", fail: " FAIL " };
@@ -44,7 +44,7 @@ const PLANS_PAGE = `"use client";
44
44
  // with the other exports) whenever you want your own copy or layout.
45
45
  export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
46
46
  `;
47
- const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's Integration tab.
47
+ const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's Connect client section.
48
48
  CONNECT_API_KEY=
49
49
  CONNECT_CLIENT_ID=
50
50
  `;
@@ -111,7 +111,7 @@ function runInit(argv = []) {
111
111
  console.log(` ${marker} ${(0, node_path_1.relative)(root, target)} - ${scaffold.description}`);
112
112
  }
113
113
  const env = ensureEnv(root);
114
- console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's Integration tab`);
114
+ console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's Connect client section`);
115
115
  // The two steps a scaffolder must not do for you: editing an existing layout
116
116
  // means parsing and rewriting JSX someone else wrote, and the redirect URI
117
117
  // lives in Connect, not in this repo.
@@ -137,7 +137,7 @@ Three things left, all one-time:
137
137
  Tailwind roots its content scan inside node_modules and skips your app.
138
138
 
139
139
  3. Register {your origin}/api/connect/callback as a redirect URI in your
140
- application's Integration tab in Connect.
140
+ application's Connect client section in Connect.
141
141
 
142
142
  No next.config.ts changes needed - not even transpilePackages: dist/ is
143
143
  already ES2022 with "use client" hoisted, so Turbopack and webpack consume it
@@ -40,12 +40,31 @@ function formatDate(iso) {
40
40
  function useConnectAccessGate() {
41
41
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
42
42
  const { data, isLoading: entitlementsLoading } = (0, provider_js_1.useConnectEntitlements)();
43
- if (sessionLoading || (session?.user && entitlementsLoading)) {
43
+ if (sessionLoading) {
44
44
  return { state: "loading", blocker: null, paymentRetry: null };
45
45
  }
46
46
  if (!session?.user) {
47
47
  return { state: "signed_out", blocker: null, paymentRetry: null };
48
48
  }
49
+ // Connect platform staff pass straight through, and this check has to come
50
+ // *before* the entitlements read below or the gate would hard-block them.
51
+ //
52
+ // Every state this gate can report is a fact about a workspace - suspended,
53
+ // no plan, wrong audience, awaiting approval, payment due. A staff session
54
+ // has no workspace at all (see `SessionAccess.staff`), so it has no subscription
55
+ // either, and `subscription_missing` would send an operator to your plan
56
+ // picker to buy a plan for a workspace that does not exist. "Nothing is
57
+ // blocking this session" is the truthful answer, not a lenient one.
58
+ //
59
+ // The entitlements request is still issued - hooks cannot be called
60
+ // conditionally - and the bridge answers it `403 no_workspace`. That costs
61
+ // one failed request per staff page load, and its result is ignored here.
62
+ if (session.staff) {
63
+ return { state: "clear", blocker: null, paymentRetry: null };
64
+ }
65
+ if (entitlementsLoading) {
66
+ return { state: "loading", blocker: null, paymentRetry: null };
67
+ }
49
68
  if (!data) {
50
69
  // Signed in, not loading, yet nothing came back - a failed read (see
51
70
  // `error` on `useConnectEntitlements`). Fail open rather than hard-block
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchConnectScreen = matchConnectScreen;
4
+ exports.isLinkableConnectScreen = isLinkableConnectScreen;
5
+ /**
6
+ * Matching a live path against the screens your application declared.
7
+ *
8
+ * The same rule Connect uses for its own routes, and it has to be here too
9
+ * because only this side knows what your app's routes are: a dynamic segment
10
+ * matches exactly one path segment, and where several patterns fit, the most
11
+ * *literal* wins - so `/deals/new` resolves to itself rather than to
12
+ * `/deals/[dealId]` merely because that also fits.
13
+ *
14
+ * Plain functions with no React and no imports beyond the type, so the panel,
15
+ * the action links and any host that wants to reuse the matching can share one
16
+ * implementation.
17
+ */
18
+ /** Trims the query string, the hash and any trailing slash. */
19
+ function normalise(path) {
20
+ const withoutQuery = path.split(/[?#]/)[0] ?? "";
21
+ return withoutQuery.length > 1 ? withoutQuery.replace(/\/+$/, "") : withoutQuery;
22
+ }
23
+ /**
24
+ * The declared screen a live path is on, or `null` if none matches.
25
+ *
26
+ * `null` is a normal outcome, not an error: an application need only describe
27
+ * the screens it wants the assistant to know about, and an undescribed one
28
+ * simply costs the assistant its orientation rather than costing the user an
29
+ * answer.
30
+ */
31
+ function matchConnectScreen(screens, path) {
32
+ const segments = normalise(path).split("/").filter(Boolean);
33
+ let best = null;
34
+ for (const screen of screens) {
35
+ const pattern = normalise(screen.path).split("/").filter(Boolean);
36
+ if (pattern.length !== segments.length)
37
+ continue;
38
+ let literals = 0;
39
+ let matches = true;
40
+ for (let i = 0; i < pattern.length; i++) {
41
+ const part = pattern[i];
42
+ if (part.startsWith("[") && part.endsWith("]"))
43
+ continue;
44
+ if (part !== segments[i]) {
45
+ matches = false;
46
+ break;
47
+ }
48
+ literals++;
49
+ }
50
+ if (matches && (!best || literals > best.literals))
51
+ best = { screen, literals };
52
+ }
53
+ return best?.screen ?? null;
54
+ }
55
+ /**
56
+ * Whether an href the model produced may be rendered as a link.
57
+ *
58
+ * Same reasoning as Connect's own `showActions` check, and the same conclusion:
59
+ * an assistant that can emit an arbitrary URL is a redirect the model can be
60
+ * talked into, and retrieved content shares a context window with its
61
+ * tool-calling authority. So a link is drawn only when it resolves to a screen
62
+ * *your application declared* - which makes an external destination structurally
63
+ * impossible, since no route pattern has a scheme or a host.
64
+ *
65
+ * The protocol-relative form is refused separately: `//evil.example.com` has no
66
+ * scheme for a path matcher to trip over, and would otherwise reach an anchor
67
+ * as an absolute destination.
68
+ */
69
+ function isLinkableConnectScreen(screens, href) {
70
+ if (!href.startsWith("/") || href.startsWith("//"))
71
+ return false;
72
+ return matchConnectScreen(screens, href) !== null;
73
+ }
@@ -8,10 +8,12 @@ exports.AiTimeline = AiTimeline;
8
8
  exports.AiDetails = AiDetails;
9
9
  exports.AiSteps = AiSteps;
10
10
  exports.AiSources = AiSources;
11
+ exports.AiActions = AiActions;
11
12
  const jsx_runtime_1 = require("react/jsx-runtime");
12
13
  const react_1 = require("react");
13
14
  const react_2 = require("@heroui/react");
14
15
  const lucide_react_1 = require("lucide-react");
16
+ const ai_screens_js_1 = require("./ai-screens.js");
15
17
  const ai_format_js_1 = require("./ai-format.js");
16
18
  /**
17
19
  * What Ask AI draws.
@@ -230,3 +232,38 @@ function AiSources({ sources }) {
230
232
  return ((0, jsx_runtime_1.jsx)("li", { children: source.uri ? ((0, jsx_runtime_1.jsxs)("a", { href: source.uri, target: "_blank", rel: "noopener noreferrer", className: "flex gap-1.5 rounded-sm hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: [label, (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "mt-0.5 size-3 shrink-0 text-muted", "aria-hidden": true })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "flex gap-1.5", children: label })) }, `${source.uri ?? source.title}-${index}`));
231
233
  }) })] }));
232
234
  }
235
+ /**
236
+ * Where to go next, as links into *your* application.
237
+ *
238
+ * ## Why links and not navigation
239
+ *
240
+ * The obvious version of "the assistant can act" moves the user. It is also the
241
+ * version that breaks the property the whole design rests on: nothing happens
242
+ * without a click. Retrieved content shares a context window with the model's
243
+ * tool-calling authority, so an auto-navigate is a redirect primitive that
244
+ * content can reach. A link cannot be, and loses nothing - the user was going
245
+ * to click something anyway.
246
+ *
247
+ * ## Why every href is checked against your declared screens
248
+ *
249
+ * The model can emit any string. Only your application knows which strings are
250
+ * real routes, which is why `screens` is a prop rather than something Connect
251
+ * could supply. A link that does not resolve is *dropped* rather than rendered
252
+ * broken - the prose around it still stands, so a missing link reads as terse
253
+ * rather than wrong.
254
+ *
255
+ * `renderLink` is how this stays router-agnostic. Given one, a link is your
256
+ * framework's - a Next `<Link>`, a TanStack `<Link>` - and navigation is a
257
+ * client transition. Without one it falls back to an anchor, which works
258
+ * everywhere and costs a document load.
259
+ */
260
+ function AiActions({ title, actions, screens, renderLink, }) {
261
+ const resolved = actions.filter((action) => (0, ai_screens_js_1.isLinkableConnectScreen)(screens, action.href));
262
+ if (resolved.length === 0)
263
+ return null;
264
+ const className = "group flex items-center gap-2 rounded-lg border border-separator bg-surface px-3 py-2 no-underline transition-colors hover:border-accent";
265
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "my-3", children: [title ? (0, jsx_runtime_1.jsx)("p", { className: "mb-1.5 text-xs font-medium text-muted", children: title }) : null, (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-1.5", children: resolved.map((action) => {
266
+ const body = ((0, jsx_runtime_1.jsxs)("span", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("span", { className: "block truncate text-xs font-medium text-foreground", children: action.label }), action.detail ? ((0, jsx_runtime_1.jsx)("span", { className: "block truncate text-[11px] text-muted", children: action.detail })) : null] }));
267
+ return ((0, jsx_runtime_1.jsx)(react_1.Fragment, { children: renderLink ? (renderLink({ href: action.href, className, children: body })) : ((0, jsx_runtime_1.jsx)("a", { href: action.href, className: className, children: body })) }, action.href));
268
+ }) })] }));
269
+ }