<!-- CURSOR_SUMMARY --> > [!NOTE] > Adds support for `dyad://add-mcp-server` deep links that prefill MCP server settings, and updates deep link context/consumers to use timestamp-based effects and clearing to avoid repeat handling. > > - **Deep Link Infrastructure**: > - Introduce `src/ipc/deep_link_data.ts` with zod schema (`AddMcpServerConfigSchema`) and typed `DeepLinkData`. > - Extend `DeepLinkContext` with `clearLastDeepLink`, timestamped events, and auto-navigate to `/settings#tools-mcp` on `add-mcp-server`. > - **Main Process**: > - Handle `dyad://add-mcp-server?name=...&config=...`: > - Base64-decode and validate `config`; send `deep-link-received` with typed payload or show error. > - **Settings UI (MCP)**: > - In `ToolsMcpSettings`, prefill form from `add-mcp-server` payload (supports `stdio` command/args and `http` url) and show info toast; clear deep link after handling. > - **Connectors/UI**: > - Update `TitleBar`, `NeonConnector`, `SupabaseConnector` to: > - Depend on `lastDeepLink?.timestamp` and call `clearLastDeepLink()` after handling (`dyad-pro-return`, `neon-oauth-return`, `supabase-oauth-return`). > - **IPC Renderer**: > - Use centralized `DeepLinkData` types in `ipc_client.ts`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 294a9c6f38442241b54e9bcbe19a7a772d338ee0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
245 lines
7.0 KiB
TypeScript
245 lines
7.0 KiB
TypeScript
import { useAtom } from "jotai";
|
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
|
import { useLoadApps } from "@/hooks/useLoadApps";
|
|
import { useRouter, useLocation } from "@tanstack/react-router";
|
|
import { useSettings } from "@/hooks/useSettings";
|
|
import { Button } from "@/components/ui/button";
|
|
// @ts-ignore
|
|
import logo from "../../assets/logo.svg";
|
|
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
|
|
import { cn } from "@/lib/utils";
|
|
import { useDeepLink } from "@/contexts/DeepLinkContext";
|
|
import { useEffect, useState } from "react";
|
|
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
|
|
import { useTheme } from "@/contexts/ThemeContext";
|
|
import { IpcClient } from "@/ipc/ipc_client";
|
|
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
|
|
import { UserBudgetInfo } from "@/ipc/ipc_types";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { ActionHeader } from "@/components/preview_panel/ActionHeader";
|
|
|
|
export const TitleBar = () => {
|
|
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
|
const { apps } = useLoadApps();
|
|
const { navigate } = useRouter();
|
|
const location = useLocation();
|
|
const { settings, refreshSettings } = useSettings();
|
|
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
|
|
const [showWindowControls, setShowWindowControls] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Check if we're running on Windows
|
|
const checkPlatform = async () => {
|
|
try {
|
|
const platform = await IpcClient.getInstance().getSystemPlatform();
|
|
setShowWindowControls(platform !== "darwin");
|
|
} catch (error) {
|
|
console.error("Failed to get platform info:", error);
|
|
}
|
|
};
|
|
|
|
checkPlatform();
|
|
}, []);
|
|
|
|
const showDyadProSuccessDialog = () => {
|
|
setIsSuccessDialogOpen(true);
|
|
};
|
|
|
|
const { lastDeepLink, clearLastDeepLink } = useDeepLink();
|
|
useEffect(() => {
|
|
const handleDeepLink = async () => {
|
|
if (lastDeepLink?.type === "dyad-pro-return") {
|
|
await refreshSettings();
|
|
showDyadProSuccessDialog();
|
|
clearLastDeepLink();
|
|
}
|
|
};
|
|
handleDeepLink();
|
|
}, [lastDeepLink?.timestamp]);
|
|
|
|
// Get selected app name
|
|
const selectedApp = apps.find((app) => app.id === selectedAppId);
|
|
const displayText = selectedApp
|
|
? `App: ${selectedApp.name}`
|
|
: "(no app selected)";
|
|
|
|
const handleAppClick = () => {
|
|
if (selectedApp) {
|
|
navigate({ to: "/app-details", search: { appId: selectedApp.id } });
|
|
}
|
|
};
|
|
|
|
const isDyadPro = !!settings?.providerSettings?.auto?.apiKey?.value;
|
|
const isDyadProEnabled = Boolean(settings?.enableDyadPro);
|
|
|
|
return (
|
|
<>
|
|
<div className="@container z-11 w-full h-11 bg-(--sidebar) absolute top-0 left-0 app-region-drag flex items-center">
|
|
<div className={`${showWindowControls ? "pl-2" : "pl-18"}`}></div>
|
|
|
|
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-0.5" />
|
|
<Button
|
|
data-testid="title-bar-app-name-button"
|
|
variant="outline"
|
|
size="sm"
|
|
className={`hidden @2xl:block no-app-region-drag text-xs max-w-38 truncate font-medium ${
|
|
selectedApp ? "cursor-pointer" : ""
|
|
}`}
|
|
onClick={handleAppClick}
|
|
>
|
|
{displayText}
|
|
</Button>
|
|
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
|
|
|
|
{/* Preview Header */}
|
|
{location.pathname === "/chat" && (
|
|
<div className="flex-1 flex justify-end">
|
|
<ActionHeader />
|
|
</div>
|
|
)}
|
|
|
|
{showWindowControls && <WindowsControls />}
|
|
</div>
|
|
|
|
<DyadProSuccessDialog
|
|
isOpen={isSuccessDialogOpen}
|
|
onClose={() => setIsSuccessDialogOpen(false)}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
function WindowsControls() {
|
|
const { isDarkMode } = useTheme();
|
|
const ipcClient = IpcClient.getInstance();
|
|
|
|
const minimizeWindow = () => {
|
|
ipcClient.minimizeWindow();
|
|
};
|
|
|
|
const maximizeWindow = () => {
|
|
ipcClient.maximizeWindow();
|
|
};
|
|
|
|
const closeWindow = () => {
|
|
ipcClient.closeWindow();
|
|
};
|
|
|
|
return (
|
|
<div className="ml-auto flex no-app-region-drag">
|
|
<button
|
|
className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
|
onClick={minimizeWindow}
|
|
aria-label="Minimize"
|
|
>
|
|
<svg
|
|
width="12"
|
|
height="1"
|
|
viewBox="0 0 12 1"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<rect
|
|
width="12"
|
|
height="1"
|
|
fill={isDarkMode ? "#ffffff" : "#000000"}
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
|
onClick={maximizeWindow}
|
|
aria-label="Maximize"
|
|
>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 12 12"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<rect
|
|
x="0.5"
|
|
y="0.5"
|
|
width="11"
|
|
height="11"
|
|
stroke={isDarkMode ? "#ffffff" : "#000000"}
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
className="w-10 h-10 flex items-center justify-center hover:bg-red-500 transition-colors"
|
|
onClick={closeWindow}
|
|
aria-label="Close"
|
|
>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 12 12"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M1 1L11 11M1 11L11 1"
|
|
stroke={isDarkMode ? "#ffffff" : "#000000"}
|
|
strokeWidth="1.5"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DyadProButton({
|
|
isDyadProEnabled,
|
|
}: {
|
|
isDyadProEnabled: boolean;
|
|
}) {
|
|
const { navigate } = useRouter();
|
|
const { userBudget } = useUserBudgetInfo();
|
|
return (
|
|
<Button
|
|
data-testid="title-bar-dyad-pro-button"
|
|
onClick={() => {
|
|
navigate({
|
|
to: providerSettingsRoute.id,
|
|
params: { provider: "auto" },
|
|
});
|
|
}}
|
|
variant="outline"
|
|
className={cn(
|
|
"hidden @2xl:block ml-1 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white text-xs px-2 pt-1 pb-1",
|
|
!isDyadProEnabled && "bg-zinc-600 dark:bg-zinc-600",
|
|
)}
|
|
size="sm"
|
|
>
|
|
{isDyadProEnabled ? "Pro" : "Pro (off)"}
|
|
{userBudget && isDyadProEnabled && (
|
|
<AICreditStatus userBudget={userBudget} />
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
export function AICreditStatus({ userBudget }: { userBudget: UserBudgetInfo }) {
|
|
const remaining = Math.round(
|
|
userBudget.totalCredits - userBudget.usedCredits,
|
|
);
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<div className="text-xs pl-1 mt-0.5">{remaining} credits</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<div>
|
|
<p>Note: there is a slight delay in updating the credit status.</p>
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|