@@ -23,14 +23,17 @@ import {
2323} from "@corbits/plugins-ui" ;
2424import type { ResolvedPlugin } from "@workbench/connections/plugins" ;
2525import { listPluginsForTenant } from "@workbench/connections/plugins" ;
26+ import { CONNECTOR_REGISTRY } from "@workbench/connections/registry" ;
27+ import { MCP_PRESETS } from "@workbench/connections/mcp-presets" ;
2628import { Plus , SquaresFour , Warning } from "@corbits/icons" ;
27- import { useCallback , useEffect , useState } from "react" ;
29+ import { useCallback , useEffect , useRef , useState } from "react" ;
2830
2931import { useBench } from "../bench-context" ;
3032import { SKILLS_PATH_PREFIX } from "../path-ids" ;
3133import {
3234 useClearPendingConnectProvider ,
3335 usePendingConnectProvider ,
36+ useRequestPluginsConnect ,
3437} from "../shell/provider-health-context" ;
3538import { StageTopBar } from "../shell/stage-top-bar" ;
3639import {
@@ -85,40 +88,79 @@ export function PluginsRoute({
8588 // link silently no-oping, this renders a notice pointing back at the
8689 // gallery itself.
8790 const [ connectDeepLinkNotFound , setConnectDeepLinkNotFound ] = useState ( false ) ;
91+ // A preset deep link's slug (CL-7141), passed to the MCP presets
92+ // section so it can focus that preset's own card once its catalog
93+ // has loaded — cleared as soon as the section has acted on it.
94+ const [ autoConnectPresetSlug , setAutoConnectPresetSlug ] = useState <
95+ string | null
96+ > ( null ) ;
8897 const pendingConnectProvider = usePendingConnectProvider ( ) ;
8998 const clearPendingConnectProvider = useClearPendingConnectProvider ( ) ;
99+ const requestPluginsConnect = useRequestPluginsConnect ( ) ;
90100 const openPluginPanel = useCallback ( ( plugin : ResolvedPlugin ) => {
91101 setOpenPlugin ( plugin ) ;
92102 setConnectDeepLinkNotFound ( false ) ;
93103 } , [ ] ) ;
94104
105+ const [ pluginsReloadKey , setPluginsReloadKey ] = useState ( 0 ) ;
106+ const [ skillsReloadKey , setSkillsReloadKey ] = useState ( 0 ) ;
107+
95108 const reloadPlugins = useCallback ( ( ) => {
96- if ( selectedTenantId === null ) return ;
97- listPluginsForTenant ( selectedTenantId )
98- . then ( ( plugins ) => setPluginsState ( { status : "ready" , plugins } ) )
99- . catch ( ( cause : unknown ) =>
100- setPluginsState ( { status : "error" , message : messageOf ( cause ) } ) ,
101- ) ;
102- } , [ selectedTenantId ] ) ;
109+ setPluginsReloadKey ( ( key ) => key + 1 ) ;
110+ } , [ ] ) ;
103111
104112 const reloadSkills = useCallback ( ( ) => {
105- if ( selectedTenantId === null ) return ;
106- listSkills ( selectedTenantId )
107- . then ( ( skills ) => setSkillsState ( { status : "ready" , skills } ) )
108- . catch ( ( cause : unknown ) =>
109- setSkillsState ( { status : "error" , message : messageOf ( cause ) } ) ,
110- ) ;
111- } , [ selectedTenantId ] ) ;
113+ setSkillsReloadKey ( ( key ) => key + 1 ) ;
114+ } , [ ] ) ;
115+
116+ // Guarded by `cancelled` (same pattern as settings-ui's `people-section`)
117+ // so a tenant switch mid-flight can't have the previous tenant's late
118+ // response overwrite the newly selected tenant's state. The loading
119+ // skeleton only shows for a tenant this page hasn't fetched yet — an
120+ // imperative reload (a connect/disconnect's `onChanged`, the error
121+ // screen's Retry) keeps whatever is already on screen and swaps in the
122+ // fresh data once it lands, the same way it worked before cancellation
123+ // was added.
124+ const pluginsLoadedTenantRef = useRef < string | null > ( null ) ;
125+ const skillsLoadedTenantRef = useRef < string | null > ( null ) ;
112126
113127 useEffect ( ( ) => {
114- setPluginsState ( { status : "loading" } ) ;
115- reloadPlugins ( ) ;
116- } , [ reloadPlugins ] ) ;
128+ if ( selectedTenantId === null ) return ;
129+ let cancelled = false ;
130+ const isTenantChange = pluginsLoadedTenantRef . current !== selectedTenantId ;
131+ pluginsLoadedTenantRef . current = selectedTenantId ;
132+ if ( isTenantChange ) setPluginsState ( { status : "loading" } ) ;
133+ listPluginsForTenant ( selectedTenantId )
134+ . then ( ( plugins ) => {
135+ if ( ! cancelled ) setPluginsState ( { status : "ready" , plugins } ) ;
136+ } )
137+ . catch ( ( cause : unknown ) => {
138+ if ( ! cancelled )
139+ setPluginsState ( { status : "error" , message : messageOf ( cause ) } ) ;
140+ } ) ;
141+ return ( ) => {
142+ cancelled = true ;
143+ } ;
144+ } , [ selectedTenantId , pluginsReloadKey ] ) ;
117145
118146 useEffect ( ( ) => {
119- setSkillsState ( { status : "loading" } ) ;
120- reloadSkills ( ) ;
121- } , [ reloadSkills ] ) ;
147+ if ( selectedTenantId === null ) return ;
148+ let cancelled = false ;
149+ const isTenantChange = skillsLoadedTenantRef . current !== selectedTenantId ;
150+ skillsLoadedTenantRef . current = selectedTenantId ;
151+ if ( isTenantChange ) setSkillsState ( { status : "loading" } ) ;
152+ listSkills ( selectedTenantId )
153+ . then ( ( skills ) => {
154+ if ( ! cancelled ) setSkillsState ( { status : "ready" , skills } ) ;
155+ } )
156+ . catch ( ( cause : unknown ) => {
157+ if ( ! cancelled )
158+ setSkillsState ( { status : "error" , message : messageOf ( cause ) } ) ;
159+ } ) ;
160+ return ( ) => {
161+ cancelled = true ;
162+ } ;
163+ } , [ selectedTenantId , skillsReloadKey ] ) ;
122164
123165 // The shell banner's "Fix it" deep link (CL-6092): once the gallery has
124166 // loaded, pick up any pending provider id and open its connect panel —
@@ -143,6 +185,42 @@ export function PluginsRoute({
143185 openPluginPanel ,
144186 ] ) ;
145187
188+ // `request_connection`'s fallback link (CL-7141): `/plugins?connect=<id>`
189+ // hands the connector id off through the same `requestPluginsConnect`
190+ // path the shell banner's "Fix it" click uses, then strips only the
191+ // `connect` param — any other query param this route is ever opened
192+ // with (e.g. an in-flight `mcpOauth` return) must survive the rewrite.
193+ // A curated MCP preset (Exa, Granola, Linear, ...) has no fixed
194+ // `CONNECTOR_REGISTRY` id, so its own deep link is `mcp:<slug>`
195+ // (`presetDeepLink` in `packages/connections-tools/src/tool.ts`) and
196+ // is matched against the preset catalog instead. An id neither side
197+ // recognizes (typo, stale link) is ignored rather than surfaced as a
198+ // notice.
199+ useEffect ( ( ) => {
200+ const params = new URLSearchParams ( window . location . search ) ;
201+ const connectId = params . get ( "connect" ) ;
202+ if ( connectId === null ) return ;
203+ params . delete ( "connect" ) ;
204+ const rest = params . toString ( ) ;
205+ window . history . replaceState (
206+ null ,
207+ "" ,
208+ rest === ""
209+ ? window . location . pathname
210+ : `${ window . location . pathname } ?${ rest } ` ,
211+ ) ;
212+ if ( connectId . startsWith ( "mcp:" ) ) {
213+ const slug = connectId . slice ( "mcp:" . length ) ;
214+ if ( MCP_PRESETS . some ( ( preset ) => preset . slug === slug ) ) {
215+ setAutoConnectPresetSlug ( slug ) ;
216+ }
217+ return ;
218+ }
219+ if ( CONNECTOR_REGISTRY [ connectId ] !== undefined ) {
220+ requestPluginsConnect ( connectId ) ;
221+ }
222+ } , [ requestPluginsConnect ] ) ;
223+
146224 function openSkill ( name : string ) {
147225 navigate ( `${ SKILLS_PATH_PREFIX } /${ encodeURIComponent ( name ) } ` ) ;
148226 }
@@ -256,6 +334,8 @@ export function PluginsRoute({
256334 activeTab = { activeTab }
257335 onTabChange = { setActiveTab }
258336 query = { galleryQuery }
337+ autoConnectPresetSlug = { autoConnectPresetSlug }
338+ onAutoConnectPresetHandled = { ( ) => setAutoConnectPresetSlug ( null ) }
259339 />
260340 </ PageShell >
261341 < PluginConnectPanel
0 commit comments