- Sources page: render McpInstallCard above the connected sources list so agent-pairing is the primary affordance on an empty workspace. - Add optional `preload` to SourcePlugin and call it from ExecutorPluginsProvider on mount, so navigating into a plugin's add/edit page no longer trips Suspense waiting on the lazy chunk. - Drop the visible "Loading…" Suspense fallback in sources-add — the preload now covers warm sessions, and a flash of nothing is better than a layout-shifting label on cold ones.
Unified · Split
| @@ -13,6 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | createContext, | 13 | createContext, |
| 14 | createElement, | 14 | createElement, |
| 15 | useContext, | 15 | useContext, |
| 16 | useEffect, | ||
| 16 | useMemo, | 17 | useMemo, |
| 17 | type ComponentType, | 18 | type ComponentType, |
| 18 | type ReactNode, | 19 | type ReactNode, |
| @@ -130,6 +131,10 @@ export interface SourcePlugin { · 111 unmodified lines | |||
| 130 | readonly sourceId: string; | 131 | readonly sourceId: string; |
| 131 | }>; | 132 | }>; |
| 132 | readonly presets?: readonly SourcePreset[]; | 133 | readonly presets?: readonly SourcePreset[]; |
| 134 | /** Trigger early download of the plugin's lazy component chunks (add/edit/etc.). | ||
| 135 | * Call from the host on intent (hover/focus) so the chunks land before the | ||
| 136 | * user navigates into the add page. Idempotent. */ | ||
| 137 | readonly preload?: () => void; | ||
| 133 | } | 138 | } |
| 134 | 139 | ||
| 135 | // --------------------------------------------------------------------------- | 140 | // --------------------------------------------------------------------------- |
| @@ -269,6 +274,11 @@ export function ExecutorPluginsProvider( · 133 unmodified lines | |||
| 269 | }), | 274 | }), |
| 270 | [plugins], | 275 | [plugins], |
| 271 | ); | 276 | ); |
| 277 | // Kick off lazy chunk downloads for every source plugin once the host | ||
| 278 | // mounts, so navigating into an add/edit page doesn't suspend. | ||
| 279 | useEffect(() => { | ||
| 280 | for (const sp of value.sourcePlugins) sp.preload?.(); | ||
| 281 | }, [value.sourcePlugins]); | ||
| 272 | return createElement(ExecutorPluginsContext.Provider, { value }, children); | 282 | return createElement(ExecutorPluginsContext.Provider, { value }, children); |
| 273 | } | 283 | } |
| 274 | 284 | ||
| @@ -2,12 +2,23 @@ import { lazy } from "react"; · 1 unmodified line | |||
| 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; | 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; |
| 3 | import { googleDiscoveryPresets } from "../sdk/presets"; | 3 | import { googleDiscoveryPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddGoogleDiscoverySource"); | ||
| 6 | const importEdit = () => import("./EditGoogleDiscoverySource"); | ||
| 7 | const importSummary = () => import("./GoogleDiscoverySourceSummary"); | ||
| 8 | const importSignIn = () => import("./GoogleDiscoverySignInButton"); | ||
| 9 | |||
| 5 | export const googleDiscoverySourcePlugin: SourcePlugin = { | 10 | export const googleDiscoverySourcePlugin: SourcePlugin = { |
| 6 | key: "googleDiscovery", | 11 | key: "googleDiscovery", |
| 7 | label: "Google Discovery", | 12 | label: "Google Discovery", |
| 8 | add: lazy(() => import("./AddGoogleDiscoverySource")), | 13 | add: lazy(importAdd), |
| 9 | edit: lazy(() => import("./EditGoogleDiscoverySource")), | 14 | edit: lazy(importEdit), |
| 10 | summary: lazy(() => import("./GoogleDiscoverySourceSummary")), | 15 | summary: lazy(importSummary), |
| 11 | signIn: lazy(() => import("./GoogleDiscoverySignInButton")), | 16 | signIn: lazy(importSignIn), |
| 12 | presets: googleDiscoveryPresets, | 17 | presets: googleDiscoveryPresets, |
| 18 | preload: () => { | ||
| 19 | void importAdd(); | ||
| 20 | void importEdit(); | ||
| 21 | void importSummary(); | ||
| 22 | void importSignIn(); | ||
| 23 | }, | ||
| 13 | }; | 24 | }; |
| @@ -2,12 +2,23 @@ import { lazy } from "react"; · 1 unmodified line | |||
| 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; | 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; |
| 3 | import { graphqlPresets } from "../sdk/presets"; | 3 | import { graphqlPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddGraphqlSource"); | ||
| 6 | const importEdit = () => import("./EditGraphqlSource"); | ||
| 7 | const importSummary = () => import("./GraphqlSourceSummary"); | ||
| 8 | const importSignIn = () => import("./GraphqlSignInButton"); | ||
| 9 | |||
| 5 | export const graphqlSourcePlugin: SourcePlugin = { | 10 | export const graphqlSourcePlugin: SourcePlugin = { |
| 6 | key: "graphql", | 11 | key: "graphql", |
| 7 | label: "GraphQL", | 12 | label: "GraphQL", |
| 8 | add: lazy(() => import("./AddGraphqlSource")), | 13 | add: lazy(importAdd), |
| 9 | edit: lazy(() => import("./EditGraphqlSource")), | 14 | edit: lazy(importEdit), |
| 10 | summary: lazy(() => import("./GraphqlSourceSummary")), | 15 | summary: lazy(importSummary), |
| 11 | signIn: lazy(() => import("./GraphqlSignInButton")), | 16 | signIn: lazy(importSignIn), |
| 12 | presets: graphqlPresets, | 17 | presets: graphqlPresets, |
| 18 | preload: () => { | ||
| 19 | void importAdd(); | ||
| 20 | void importEdit(); | ||
| 21 | void importSummary(); | ||
| 22 | void importSignIn(); | ||
| 23 | }, | ||
| 13 | }; | 24 | }; |
| @@ -2,9 +2,13 @@ import { lazy, type ComponentProps, type ComponentType } from "react"; · 1 unmodified line | |||
| 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; | 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; |
| 3 | import { mcpPresets } from "../sdk/presets"; | 3 | import { mcpPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const LazyAddMcpSource = lazy(() => import("./AddMcpSource")); | 5 | const importAdd = () => import("./AddMcpSource"); |
| 6 | const LazyEditMcpSource = lazy(() => import("./EditMcpSource")); | 6 | const importEdit = () => import("./EditMcpSource"); |
| 7 | const LazyMcpSignInButton = lazy(() => import("./McpSignInButton")); | 7 | const importSignIn = () => import("./McpSignInButton"); |
| 8 | |||
| 9 | const LazyAddMcpSource = lazy(importAdd); | ||
| 10 | const LazyEditMcpSource = lazy(importEdit); | ||
| 11 | const LazyMcpSignInButton = lazy(importSignIn); | ||
| 8 | 12 | ||
| 9 | type AddProps = ComponentProps<SourcePlugin["add"]>; | 13 | type AddProps = ComponentProps<SourcePlugin["add"]>; |
| 10 | 14 | ||
| @@ -41,6 +45,11 @@ export const createMcpSourcePlugin = ( · 30 unmodified lines | |||
| 41 | edit: LazyEditMcpSource, | 45 | edit: LazyEditMcpSource, |
| 42 | signIn: LazyMcpSignInButton, | 46 | signIn: LazyMcpSignInButton, |
| 43 | presets, | 47 | presets, |
| 48 | preload: () => { | ||
| 49 | void importAdd(); | ||
| 50 | void importEdit(); | ||
| 51 | void importSignIn(); | ||
| 52 | }, | ||
| 44 | }; | 53 | }; |
| 45 | }; | 54 | }; |
| 46 | 55 | ||
| @@ -2,11 +2,20 @@ import { lazy } from "react"; · 1 unmodified line | |||
| 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; | 2 | import type { SourcePlugin } from "@executor-js/sdk/client"; |
| 3 | import { openApiPresets } from "../sdk/presets"; | 3 | import { openApiPresets } from "../sdk/presets"; |
| 4 | 4 | ||
| 5 | const importAdd = () => import("./AddOpenApiSource"); | ||
| 6 | const importEdit = () => import("./EditOpenApiSource"); | ||
| 7 | const importSummary = () => import("./OpenApiSourceSummary"); | ||
| 8 | |||
| 5 | export const openApiSourcePlugin: SourcePlugin = { | 9 | export const openApiSourcePlugin: SourcePlugin = { |
| 6 | key: "openapi", | 10 | key: "openapi", |
| 7 | label: "OpenAPI", | 11 | label: "OpenAPI", |
| 8 | add: lazy(() => import("./AddOpenApiSource")), | 12 | add: lazy(importAdd), |
| 9 | edit: lazy(() => import("./EditOpenApiSource")), | 13 | edit: lazy(importEdit), |
| 10 | summary: lazy(() => import("./OpenApiSourceSummary")), | 14 | summary: lazy(importSummary), |
| 11 | presets: openApiPresets, | 15 | presets: openApiPresets, |
| 16 | preload: () => { | ||
| 17 | void importAdd(); | ||
| 18 | void importEdit(); | ||
| 19 | void importSummary(); | ||
| 20 | }, | ||
| 12 | }; | 21 | }; |
| @@ -46,7 +46,7 @@ export function SourcesAddPage(props: { · 45 unmodified lines | |||
| 46 | return ( | 46 | return ( |
| 47 | <div className="relative min-h-0 flex-1 overflow-y-auto"> | 47 | <div className="relative min-h-0 flex-1 overflow-y-auto"> |
| 48 | <div className="mx-auto flex min-h-full max-w-4xl flex-col px-6 py-10 lg:px-10 lg:py-14"> | 48 | <div className="mx-auto flex min-h-full max-w-4xl flex-col px-6 py-10 lg:px-10 lg:py-14"> |
| 49 | <Suspense fallback={<p className="text-sm text-muted-foreground">Loading…</p>}> | 49 | <Suspense fallback={null}> |
| 50 | <AddComponent | 50 | <AddComponent |
| 51 | initialUrl={url} | 51 | initialUrl={url} |
| 52 | initialPreset={preset} | 52 | initialPreset={preset} |
| @@ -81,6 +81,12 @@ export function SourcesPage() { · 80 unmodified lines | |||
| 81 | </Button> | 81 | </Button> |
| 82 | </div> | 82 | </div> |
| 83 | 83 | ||
| 84 | <div className="mb-8"> | ||
| 85 | <McpInstallCard /> | ||
| 86 | </div> | ||
| 87 | |||
| 88 | <div className="mb-8 border-t border-border/50" /> | ||
| 89 | |||
| 84 | {AsyncResult.match(sources, { | 90 | {AsyncResult.match(sources, { |
| 85 | onInitial: () => <SourcesGridSkeleton />, | 91 | onInitial: () => <SourcesGridSkeleton />, |
| 86 | onFailure: () => <p className="text-sm text-destructive">Failed to load sources</p>, | 92 | onFailure: () => <p className="text-sm text-destructive">Failed to load sources</p>, |
| @@ -106,12 +112,6 @@ export function SourcesPage() { · 19 unmodified lines | |||
| 106 | ); | 112 | ); |
| 107 | }, | 113 | }, |
| 108 | })} | 114 | })} |
| 109 | |||
| 110 | <div className="mb-8 border-t border-border/50" /> | ||
| 111 | |||
| 112 | <div className="mb-8"> | ||
| 113 | <McpInstallCard /> | ||
| 114 | </div> | ||
| 115 | </div> | 115 | </div> |
| 116 | 116 | ||
| 117 | <ConnectDialog open={connectOpen} onOpenChange={setConnectOpen} /> | 117 | <ConnectDialog open={connectOpen} onOpenChange={setConnectOpen} /> |