Neon / portal template support (#713)

TODOs:
- [x] Do restart when checkout / restore if there is a DB
- [x] List all branches (branch id, name, date)
- [x] Allow checking out versions with no DB
- [x] safeguard to never delete main branches
- [x] create app hook for neon template
- [x] weird UX with connector on configure panel
- [x] tiny neon logo in connector
- [x] deploy to vercel
- [x] build forgot password page
- [x] what about email setup
- [x] lots of imgix errors
- [x] edit file - db snapshot
- [x] DYAD_DISABLE_DB_PUSH
- [ ] update portal doc
- [x] switch preview branch to be read-only endpoint
- [x] disable supabase sys prompt if neon is enabled
- [ ] https://payloadcms.com/docs/upload/storage-adapters
- [x] need to use main branch...

Phase 2?
- [x] generate DB migrations
This commit is contained in:
Will Chen
2025-08-04 16:36:09 -07:00
committed by GitHub
parent 0f1a5c5c77
commit b0f08eaf15
50 changed files with 3525 additions and 205 deletions

38
src/hooks/useCreateApp.ts Normal file
View File

@@ -0,0 +1,38 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client";
import { showError } from "@/lib/toast";
import type { CreateAppParams, CreateAppResult } from "@/ipc/ipc_types";
export function useCreateApp() {
const queryClient = useQueryClient();
const mutation = useMutation<CreateAppResult, Error, CreateAppParams>({
mutationFn: async (params: CreateAppParams) => {
if (!params.name.trim()) {
throw new Error("App name is required");
}
const ipcClient = IpcClient.getInstance();
return ipcClient.createApp(params);
},
onSuccess: () => {
// Invalidate apps list to trigger refetch
queryClient.invalidateQueries({ queryKey: ["apps"] });
},
onError: (error) => {
showError(error);
},
});
const createApp = async (
params: CreateAppParams,
): Promise<CreateAppResult> => {
return mutation.mutateAsync(params);
};
return {
createApp,
isCreating: mutation.isPending,
error: mutation.error,
};
}

View File

@@ -11,6 +11,7 @@ import {
} from "@/atoms/appAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { AppOutput } from "@/ipc/ipc_types";
import { showInputRequest } from "@/lib/toast";
const useRunAppLoadingAtom = atom(false);
@@ -18,7 +19,7 @@ export function useRunApp() {
const [loading, setLoading] = useAtom(useRunAppLoadingAtom);
const [app, setApp] = useAtom(currentAppAtom);
const setAppOutput = useSetAtom(appOutputAtom);
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
const [, setAppUrlObj] = useAtom(appUrlAtom);
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
const appId = useAtomValue(selectedAppIdAtom);
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
@@ -39,47 +40,78 @@ export function useRunApp() {
const originalUrl = originalUrlMatch && originalUrlMatch[1];
setAppUrlObj({
appUrl: proxyUrl,
appId: appId!,
appId: output.appId,
originalUrl: originalUrl!,
});
}
}
};
const runApp = useCallback(async (appId: number) => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
console.debug("Running app", appId);
// Clear the URL and add restart message
if (appUrlObj?.appId !== appId) {
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
const processAppOutput = useCallback(
(output: AppOutput) => {
// Handle input requests specially
if (output.type === "input-requested") {
showInputRequest(output.message, async (response) => {
try {
const ipcClient = IpcClient.getInstance();
await ipcClient.respondToAppInput({
appId: output.appId,
response,
});
} catch (error) {
console.error("Failed to respond to app input:", error);
}
});
return; // Don't add to regular output
}
setAppOutput((prev) => [
...prev,
{
message: "Trying to restart app...",
type: "stdout",
appId,
timestamp: Date.now(),
},
]);
const app = await ipcClient.getApp(appId);
setApp(app);
await ipcClient.runApp(appId, (output) => {
setAppOutput((prev) => [...prev, output]);
processProxyServerOutput(output);
});
setPreviewErrorMessage(undefined);
} catch (error) {
console.error(`Error running app ${appId}:`, error);
setPreviewErrorMessage(
error instanceof Error ? error.message : error?.toString(),
);
} finally {
setLoading(false);
}
}, []);
// Add to regular app output
setAppOutput((prev) => [...prev, output]);
// Process proxy server output
processProxyServerOutput(output);
},
[setAppOutput],
);
const runApp = useCallback(
async (appId: number) => {
setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
console.debug("Running app", appId);
// Clear the URL and add restart message
setAppUrlObj((prevAppUrlObj) => {
if (prevAppUrlObj?.appId !== appId) {
return { appUrl: null, appId: null, originalUrl: null };
}
return prevAppUrlObj; // No change needed
});
setAppOutput((prev) => [
...prev,
{
message: "Trying to restart app...",
type: "stdout",
appId,
timestamp: Date.now(),
},
]);
const app = await ipcClient.getApp(appId);
setApp(app);
await ipcClient.runApp(appId, processAppOutput);
setPreviewErrorMessage(undefined);
} catch (error) {
console.error(`Error running app ${appId}:`, error);
setPreviewErrorMessage(
error instanceof Error ? error.message : error?.toString(),
);
} finally {
setLoading(false);
}
},
[processAppOutput],
);
const stopApp = useCallback(async (appId: number) => {
if (appId === null) {
@@ -139,15 +171,15 @@ export function useRunApp() {
await ipcClient.restartApp(
appId,
(output) => {
setAppOutput((prev) => [...prev, output]);
// Handle HMR updates before processing
if (
output.message.includes("hmr update") &&
output.message.includes("[vite]")
) {
onHotModuleReload();
return;
}
processProxyServerOutput(output);
// Process normally (including input requests)
processAppOutput(output);
},
removeNodeModules,
);
@@ -161,7 +193,15 @@ export function useRunApp() {
setLoading(false);
}
},
[appId, setApp, setAppOutput, setAppUrlObj, setPreviewPanelKey],
[
appId,
setApp,
setAppOutput,
setAppUrlObj,
setPreviewPanelKey,
processAppOutput,
onHotModuleReload,
],
);
const refreshAppIframe = useCallback(async () => {

View File

@@ -5,7 +5,8 @@ import { IpcClient } from "@/ipc/ipc_client";
import { chatMessagesAtom, selectedChatIdAtom } from "@/atoms/chatAtoms";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { Version } from "@/ipc/ipc_types";
import type { RevertVersionResponse, Version } from "@/ipc/ipc_types";
import { toast } from "sonner";
export function useVersions(appId: number | null) {
const [, setVersionsAtom] = useAtom(versionsListAtom);
@@ -38,35 +39,42 @@ export function useVersions(appId: number | null) {
}
}, [versions, setVersionsAtom]);
const revertVersionMutation = useMutation<void, Error, { versionId: string }>(
{
mutationFn: async ({ versionId }: { versionId: string }) => {
const currentAppId = appId;
if (currentAppId === null) {
throw new Error("App ID is null");
}
const ipcClient = IpcClient.getInstance();
await ipcClient.revertVersion({
appId: currentAppId,
previousVersionId: versionId,
});
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["versions", appId] });
await queryClient.invalidateQueries({
queryKey: ["currentBranch", appId],
});
if (selectedChatId) {
const chat = await IpcClient.getInstance().getChat(selectedChatId);
setMessages(chat.messages);
}
await queryClient.invalidateQueries({
queryKey: ["problems", appId],
});
},
meta: { showErrorToast: true },
const revertVersionMutation = useMutation<
RevertVersionResponse,
Error,
{ versionId: string }
>({
mutationFn: async ({ versionId }: { versionId: string }) => {
const currentAppId = appId;
if (currentAppId === null) {
throw new Error("App ID is null");
}
const ipcClient = IpcClient.getInstance();
return ipcClient.revertVersion({
appId: currentAppId,
previousVersionId: versionId,
});
},
);
onSuccess: async (result) => {
if ("successMessage" in result) {
toast.success(result.successMessage);
} else if ("warningMessage" in result) {
toast.warning(result.warningMessage);
}
await queryClient.invalidateQueries({ queryKey: ["versions", appId] });
await queryClient.invalidateQueries({
queryKey: ["currentBranch", appId],
});
if (selectedChatId) {
const chat = await IpcClient.getInstance().getChat(selectedChatId);
setMessages(chat.messages);
}
await queryClient.invalidateQueries({
queryKey: ["problems", appId],
});
},
meta: { showErrorToast: true },
});
return {
versions: versions || [],
@@ -74,5 +82,6 @@ export function useVersions(appId: number | null) {
error,
refreshVersions,
revertVersion: revertVersionMutation.mutateAsync,
isRevertingVersion: revertVersionMutation.isPending,
};
}