Skip to content

Commit 64ff65a

Browse files
Merge pull request #449 from corbitsdev/cl-7141-request_connections-fallback-link-pluginsconnectltidgt-is
Wire up request_connection's Plugins deep link
2 parents 5078024 + ff589e9 commit 64ff65a

8 files changed

Lines changed: 506 additions & 48 deletions

File tree

‎apps/web/src/pages/plugins-page.tsx‎

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ import {
2323
} from "@corbits/plugins-ui";
2424
import type { ResolvedPlugin } from "@workbench/connections/plugins";
2525
import { listPluginsForTenant } from "@workbench/connections/plugins";
26+
import { CONNECTOR_REGISTRY } from "@workbench/connections/registry";
27+
import { MCP_PRESETS } from "@workbench/connections/mcp-presets";
2628
import { Plus, SquaresFour, Warning } from "@corbits/icons";
27-
import { useCallback, useEffect, useState } from "react";
29+
import { useCallback, useEffect, useRef, useState } from "react";
2830

2931
import { useBench } from "../bench-context";
3032
import { SKILLS_PATH_PREFIX } from "../path-ids";
3133
import {
3234
useClearPendingConnectProvider,
3335
usePendingConnectProvider,
36+
useRequestPluginsConnect,
3437
} from "../shell/provider-health-context";
3538
import { StageTopBar } from "../shell/stage-top-bar";
3639
import {
@@ -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

Comments
 (0)