Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3b8770a
docs: add ConnectionsSetup component design
viktormarinho Feb 23, 2026
fda1126
docs: add ConnectionsSetup implementation plan
viktormarinho Feb 23, 2026
1eefc72
feat(connections-setup): add registry-item and connection-poll query …
viktormarinho Feb 23, 2026
6ee1225
feat(connections-setup): add slot resolution pure logic with tests
viktormarinho Feb 23, 2026
64e9adc
fix(connections-setup): replace dynamic import with static import in …
viktormarinho Feb 23, 2026
089de1b
fix(connections-setup): fix misleading test description in slot-resol…
viktormarinho Feb 23, 2026
6d768c6
feat(connections-setup): add use-slot-resolution hook
viktormarinho Feb 23, 2026
4f8b36b
fix(connections-setup): fix dead null-check and improve registry erro…
viktormarinho Feb 23, 2026
9bec0c4
feat(connections-setup): add use-connection-poller hook
viktormarinho Feb 23, 2026
f903a8a
feat(connections-setup): add slot-done component
viktormarinho Feb 23, 2026
c992085
feat(connections-setup): add slot-install-form component
viktormarinho Feb 23, 2026
722bcc2
feat(connections-setup): add slot-auth-oauth component
viktormarinho Feb 23, 2026
d94b896
feat(connections-setup): add slot-auth-token component
viktormarinho Feb 23, 2026
cb6a7be
fix(connections-setup): fix poller response shape, install ID, oauth …
viktormarinho Feb 23, 2026
023147f
feat(connections-setup): add slot-card state machine component
viktormarinho Feb 23, 2026
81c3f53
feat(connections-setup): fix auth phase bug, add root component and b…
viktormarinho Feb 23, 2026
5b4eec2
fix(connections-setup): fix render-phase side effects and stuck auth …
viktormarinho Feb 24, 2026
2cca6ea
test(connections-setup): add DOM component tests with @testing-librar…
viktormarinho Feb 24, 2026
ed8a2f4
fix(connections-setup): always check auth after poller goes active
viktormarinho Feb 24, 2026
d98b5ee
fix(connections-setup): detect OAuth need via oauth_config, not just …
viktormarinho Feb 24, 2026
5e4a3b6
fix(connections-setup): remove staleTime:Infinity from auth check query
viktormarinho Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@
"marked": "^15.0.6",
"mesh-plugin-object-storage": "workspace:*",
"mesh-plugin-private-registry": "workspace:*",
"mesh-plugin-user-sandbox": "workspace:*",
"mesh-plugin-reports": "workspace:*",
"mesh-plugin-user-sandbox": "workspace:*",
"mesh-plugin-workflows": "workspace:*",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState, useRef } from "react";
import { SlotCard } from "./slot-card";
import type { ConnectionSlot } from "./use-slot-resolution";

export interface ConnectionsSetupProps {
slots: Record<string, ConnectionSlot>;
onComplete: (connections: Record<string, string>) => void;
}

export function ConnectionsSetup({ slots, onComplete }: ConnectionsSetupProps) {
const [completed, setCompleted] = useState<Record<string, string>>({});
const wasAllDoneRef = useRef(false);

const handleSlotComplete = (slotId: string, connectionId: string) => {
setCompleted((prev) => {
const next = { ...prev };
if (connectionId === "") {
delete next[slotId];
} else {
next[slotId] = connectionId;
}
return next;
});
};

const allSlotIds = Object.keys(slots);
const allDone =
allSlotIds.length > 0 && allSlotIds.every((id) => completed[id]);

if (allDone && !wasAllDoneRef.current) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Calling onComplete during render introduces a side effect and can trigger render loops if the callback updates state. Move this into a useEffect that runs when allDone transitions to true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/connections-setup/connections-setup.tsx, line 30:

<comment>Calling `onComplete` during render introduces a side effect and can trigger render loops if the callback updates state. Move this into a `useEffect` that runs when `allDone` transitions to true.</comment>

<file context>
@@ -9,22 +9,31 @@ export interface ConnectionsSetupProps {
+  const allDone =
+    allSlotIds.length > 0 && allSlotIds.every((id) => completed[id]);
+
+  if (allDone && !wasAllDoneRef.current) {
+    wasAllDoneRef.current = true;
+    onComplete(completed);
</file context>
Fix with Cubic

wasAllDoneRef.current = true;
onComplete(completed);
} else if (!allDone) {
wasAllDoneRef.current = false;
}

return (
<div className="space-y-3">
{Object.entries(slots).map(([slotId, slot]) => (
<SlotCard
key={slotId}
slot={slot}
onComplete={(connectionId) =>
handleSlotComplete(slotId, connectionId)
}
/>
))}
</div>
);
}
3 changes: 3 additions & 0 deletions apps/mesh/src/web/components/connections-setup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ConnectionsSetup } from "./connections-setup";
export type { ConnectionsSetupProps } from "./connections-setup";
export type { ConnectionSlot } from "./use-slot-resolution";
99 changes: 99 additions & 0 deletions apps/mesh/src/web/components/connections-setup/slot-auth-oauth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState } from "react";
import { toast } from "sonner";
import { authenticateMcp, useConnectionActions } from "@decocms/mesh-sdk";
import { useQueryClient } from "@tanstack/react-query";
import { KEYS } from "@/web/lib/query-keys";
import { Button } from "@deco/ui/components/button.tsx";

interface SlotAuthOAuthProps {
connectionId: string;
providerName: string;
onAuthed: () => void;
}

export function SlotAuthOAuth({
connectionId,
providerName,
onAuthed,
}: SlotAuthOAuthProps) {
const [isPending, setIsPending] = useState(false);
const actions = useConnectionActions();
const queryClient = useQueryClient();
const mcpProxyUrl = new URL(`/mcp/${connectionId}`, window.location.origin);

const handleAuthorize = async () => {
setIsPending(true);
try {
const { token, tokenInfo, error } = await authenticateMcp({
connectionId,
});

if (error || !token) {
toast.error(`Authorization failed: ${error ?? "Unknown error"}`);
return;
}

if (tokenInfo) {
try {
const response = await fetch(
`/api/connections/${connectionId}/oauth-token`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
expiresIn: tokenInfo.expiresIn,
scope: tokenInfo.scope,
clientId: tokenInfo.clientId,
clientSecret: tokenInfo.clientSecret,
tokenEndpoint: tokenInfo.tokenEndpoint,
}),
},
);
if (!response.ok) {
await actions.update.mutateAsync({
id: connectionId,
data: { connection_token: token },
});
} else {
// Trigger tool re-discovery
await actions.update.mutateAsync({ id: connectionId, data: {} });
}
} catch (err) {
console.error("Error saving OAuth token:", err);
await actions.update.mutateAsync({
id: connectionId,
data: { connection_token: token },
});
}
} else {
await actions.update.mutateAsync({
id: connectionId,
data: { connection_token: token },
});
}

await queryClient.invalidateQueries({
queryKey: KEYS.isMCPAuthenticated(mcpProxyUrl.href, null),
});

toast.success("Authorization successful");
onAuthed();
} finally {
setIsPending(false);
}
};

return (
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
Authorize Mesh to access {providerName} on your behalf.
</p>
<Button onClick={handleAuthorize} disabled={isPending} className="w-full">
{isPending ? "Authorizing..." : `Authorize with ${providerName}`}
</Button>
</div>
);
}
71 changes: 71 additions & 0 deletions apps/mesh/src/web/components/connections-setup/slot-auth-token.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useConnectionActions } from "@decocms/mesh-sdk";
import { Button } from "@deco/ui/components/button.tsx";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@deco/ui/components/form.tsx";
import { Input } from "@deco/ui/components/input.tsx";

const tokenSchema = z.object({
token: z.string().min(1, "Token is required"),
});

type TokenFormData = z.infer<typeof tokenSchema>;

interface SlotAuthTokenProps {
connectionId: string;
onAuthed: () => void;
}

export function SlotAuthToken({ connectionId, onAuthed }: SlotAuthTokenProps) {
const actions = useConnectionActions();

const form = useForm<TokenFormData>({
resolver: zodResolver(tokenSchema),
defaultValues: { token: "" },
});

const handleSubmit = async (data: TokenFormData) => {
await actions.update.mutateAsync({
id: connectionId,
data: { connection_token: data.token },
});
// Trigger tool re-discovery
await actions.update.mutateAsync({ id: connectionId, data: {} });
onAuthed();
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-3">
<FormField
control={form.control}
name="token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input type="password" placeholder="sk-..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={actions.update.isPending}
className="w-full"
>
{actions.update.isPending ? "Saving..." : "Save token"}
</Button>
</form>
</Form>
);
}
Loading