History

Move connect-agent above sources and preload plugin chunks (#494)

- 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.

Rhys Sullivan committed 6 months ago

Commit b849f921 parent c627c487 files · +71 −21Browse files

Unified · Split

7 files
  1. packages/core/sdk/src/client.ts +10 −0
  2. packages/plugins/google-discovery/src/react/source-plugin.ts +15 −4
  3. packages/plugins/graphql/src/react/source-plugin.ts +15 −4
  4. packages/plugins/mcp/src/react/source-plugin.tsx +12 −3
  5. packages/plugins/openapi/src/react/source-plugin.ts +12 −3
  6. packages/react/src/pages/sources-add.tsx +1 −1
  7. packages/react/src/pages/sources.tsx +6 −6
packages/core/sdk/src/client.ts+10−0
@@ -13,6 +13,7 @@ import { · 12 unmodified lines
1313 createContext,
1414 createElement,
1515 useContext,
16+ useEffect,
1617 useMemo,
1718 type ComponentType,
1819 type ReactNode,
@@ -130,6 +131,10 @@ export interface SourcePlugin { · 111 unmodified lines
130131 readonly sourceId: string;
131132 }>;
132133 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;
133138 }
134139
135140 // ---------------------------------------------------------------------------
@@ -269,6 +274,11 @@ export function ExecutorPluginsProvider( · 133 unmodified lines
269274 }),
270275 [plugins],
271276 );
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]);
272282 return createElement(ExecutorPluginsContext.Provider, { value }, children);
273283 }
274284
packages/plugins/google-discovery/src/react/source-plugin.ts+15−4
@@ -2,12 +2,23 @@ import { lazy } from "react"; · 1 unmodified line
22 import type { SourcePlugin } from "@executor-js/sdk/client";
33 import { googleDiscoveryPresets } from "../sdk/presets";
44
5+const importAdd = () => import("./AddGoogleDiscoverySource");
6+const importEdit = () => import("./EditGoogleDiscoverySource");
7+const importSummary = () => import("./GoogleDiscoverySourceSummary");
8+const importSignIn = () => import("./GoogleDiscoverySignInButton");
9+
510 export const googleDiscoverySourcePlugin: SourcePlugin = {
611 key: "googleDiscovery",
712 label: "Google Discovery",
8− add: lazy(() => import("./AddGoogleDiscoverySource")),
9− edit: lazy(() => import("./EditGoogleDiscoverySource")),
10− summary: lazy(() => import("./GoogleDiscoverySourceSummary")),
11− signIn: lazy(() => import("./GoogleDiscoverySignInButton")),
13+ add: lazy(importAdd),
14+ edit: lazy(importEdit),
15+ summary: lazy(importSummary),
16+ signIn: lazy(importSignIn),
1217 presets: googleDiscoveryPresets,
18+ preload: () => {
19+ void importAdd();
20+ void importEdit();
21+ void importSummary();
22+ void importSignIn();
23+ },
1324 };
packages/plugins/graphql/src/react/source-plugin.ts+15−4
@@ -2,12 +2,23 @@ import { lazy } from "react"; · 1 unmodified line
22 import type { SourcePlugin } from "@executor-js/sdk/client";
33 import { graphqlPresets } from "../sdk/presets";
44
5+const importAdd = () => import("./AddGraphqlSource");
6+const importEdit = () => import("./EditGraphqlSource");
7+const importSummary = () => import("./GraphqlSourceSummary");
8+const importSignIn = () => import("./GraphqlSignInButton");
9+
510 export const graphqlSourcePlugin: SourcePlugin = {
611 key: "graphql",
712 label: "GraphQL",
8− add: lazy(() => import("./AddGraphqlSource")),
9− edit: lazy(() => import("./EditGraphqlSource")),
10− summary: lazy(() => import("./GraphqlSourceSummary")),
11− signIn: lazy(() => import("./GraphqlSignInButton")),
13+ add: lazy(importAdd),
14+ edit: lazy(importEdit),
15+ summary: lazy(importSummary),
16+ signIn: lazy(importSignIn),
1217 presets: graphqlPresets,
18+ preload: () => {
19+ void importAdd();
20+ void importEdit();
21+ void importSummary();
22+ void importSignIn();
23+ },
1324 };
packages/plugins/mcp/src/react/source-plugin.tsx+12−3
@@ -2,9 +2,13 @@ import { lazy, type ComponentProps, type ComponentType } from "react"; · 1 unmodified line
22 import type { SourcePlugin } from "@executor-js/sdk/client";
33 import { mcpPresets } from "../sdk/presets";
44
5−const LazyAddMcpSource = lazy(() => import("./AddMcpSource"));
6−const LazyEditMcpSource = lazy(() => import("./EditMcpSource"));
7−const LazyMcpSignInButton = lazy(() => import("./McpSignInButton"));
5+const importAdd = () => import("./AddMcpSource");
6+const importEdit = () => import("./EditMcpSource");
7+const importSignIn = () => import("./McpSignInButton");
8+
9+const LazyAddMcpSource = lazy(importAdd);
10+const LazyEditMcpSource = lazy(importEdit);
11+const LazyMcpSignInButton = lazy(importSignIn);
812
913 type AddProps = ComponentProps<SourcePlugin["add"]>;
1014
@@ -41,6 +45,11 @@ export const createMcpSourcePlugin = ( · 30 unmodified lines
4145 edit: LazyEditMcpSource,
4246 signIn: LazyMcpSignInButton,
4347 presets,
48+ preload: () => {
49+ void importAdd();
50+ void importEdit();
51+ void importSignIn();
52+ },
4453 };
4554 };
4655
packages/plugins/openapi/src/react/source-plugin.ts+12−3
@@ -2,11 +2,20 @@ import { lazy } from "react"; · 1 unmodified line
22 import type { SourcePlugin } from "@executor-js/sdk/client";
33 import { openApiPresets } from "../sdk/presets";
44
5+const importAdd = () => import("./AddOpenApiSource");
6+const importEdit = () => import("./EditOpenApiSource");
7+const importSummary = () => import("./OpenApiSourceSummary");
8+
59 export const openApiSourcePlugin: SourcePlugin = {
610 key: "openapi",
711 label: "OpenAPI",
8− add: lazy(() => import("./AddOpenApiSource")),
9− edit: lazy(() => import("./EditOpenApiSource")),
10− summary: lazy(() => import("./OpenApiSourceSummary")),
12+ add: lazy(importAdd),
13+ edit: lazy(importEdit),
14+ summary: lazy(importSummary),
1115 presets: openApiPresets,
16+ preload: () => {
17+ void importAdd();
18+ void importEdit();
19+ void importSummary();
20+ },
1221 };
packages/react/src/pages/sources-add.tsx+1−1
@@ -46,7 +46,7 @@ export function SourcesAddPage(props: { · 45 unmodified lines
4646 return (
4747 <div className="relative min-h-0 flex-1 overflow-y-auto">
4848 <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}>
5050 <AddComponent
5151 initialUrl={url}
5252 initialPreset={preset}
packages/react/src/pages/sources.tsx+6−6
@@ -81,6 +81,12 @@ export function SourcesPage() { · 80 unmodified lines
8181 </Button>
8282 </div>
8383
84+ <div className="mb-8">
85+ <McpInstallCard />
86+ </div>
87+
88+ <div className="mb-8 border-t border-border/50" />
89+
8490 {AsyncResult.match(sources, {
8591 onInitial: () => <SourcesGridSkeleton />,
8692 onFailure: () => <p className="text-sm text-destructive">Failed to load sources</p>,
@@ -106,12 +112,6 @@ export function SourcesPage() { · 19 unmodified lines
106112 );
107113 },
108114 })}
109−
110− <div className="mb-8 border-t border-border/50" />
111−
112− <div className="mb-8">
113− <McpInstallCard />
114− </div>
115115 </div>
116116
117117 <ConnectDialog open={connectOpen} onOpenChange={setConnectOpen} />