Files
moreminimore-vibe/src/app/TitleBar.tsx
Will Chen 744e413e71 Support deep linking MCP (#1550)
<!-- 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 -->
2025-10-16 10:20:16 -07:00

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>
);
}