feat: integrate custom features for smart context management
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled

- Added a new integration script to manage custom features related to smart context.
- Implemented handlers for smart context operations (get, update, clear, stats) in ipc.
- Created a SmartContextStore class to manage context snippets and summaries.
- Developed hooks for React to interact with smart context (useSmartContext, useUpdateSmartContext, useClearSmartContext, useSmartContextStats).
- Included backup and restore functionality in the integration script.
- Validated integration by checking for custom modifications and file existence.
This commit is contained in:
Kunthawat Greethong
2025-12-18 15:56:48 +07:00
parent 99b0cdf8ac
commit 5660de49de
423 changed files with 70726 additions and 982 deletions

View File

@@ -0,0 +1,689 @@
import { useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
appBasePathAtom,
appsListAtom,
selectedAppIdAtom,
} from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { useLoadApps } from "@/hooks/useLoadApps";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
ArrowLeft,
MoreVertical,
MessageCircle,
Pencil,
Folder,
} from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { GitHubConnector } from "@/components/GitHubConnector";
import { SupabaseConnector } from "@/components/SupabaseConnector";
import { showError } from "@/lib/toast";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Label } from "@/components/ui/label";
import { Loader2 } from "lucide-react";
import { invalidateAppQuery } from "@/hooks/useLoadApp";
import { useDebounce } from "@/hooks/useDebounce";
import { useCheckName } from "@/hooks/useCheckName";
import { AppUpgrades } from "@/components/AppUpgrades";
import { CapacitorControls } from "@/components/CapacitorControls";
export default function AppDetailsPage() {
const navigate = useNavigate();
const router = useRouter();
const search = useSearch({ from: "/app-details" as const });
const [appsList] = useAtom(appsListAtom);
const { refreshApps } = useLoadApps();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isRenameConfirmDialogOpen, setIsRenameConfirmDialogOpen] =
useState(false);
const [newAppName, setNewAppName] = useState("");
const [isRenaming, setIsRenaming] = useState(false);
const [isRenameFolderDialogOpen, setIsRenameFolderDialogOpen] =
useState(false);
const [newFolderName, setNewFolderName] = useState("");
const [isRenamingFolder, setIsRenamingFolder] = useState(false);
const appBasePath = useAtomValue(appBasePathAtom);
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
const [newCopyAppName, setNewCopyAppName] = useState("");
const queryClient = useQueryClient();
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const debouncedNewCopyAppName = useDebounce(newCopyAppName, 150);
const { data: checkNameResult, isLoading: isCheckingName } = useCheckName(
debouncedNewCopyAppName,
);
const nameExists = checkNameResult?.exists ?? false;
// Get the appId from search params and find the corresponding app
const appId = search.appId ? Number(search.appId) : null;
const selectedApp = appId ? appsList.find((app) => app.id === appId) : null;
const handleDeleteApp = async () => {
if (!appId) return;
try {
setIsDeleting(true);
await IpcClient.getInstance().deleteApp(appId);
setIsDeleteDialogOpen(false);
await refreshApps();
navigate({ to: "/", search: {} });
} catch (error) {
setIsDeleteDialogOpen(false);
showError(error);
} finally {
setIsDeleting(false);
}
};
const handleOpenRenameDialog = () => {
if (selectedApp) {
setNewAppName(selectedApp.name);
setIsRenameDialogOpen(true);
}
};
const handleOpenRenameFolderDialog = () => {
if (selectedApp) {
setNewFolderName(selectedApp.path.split("/").pop() || selectedApp.path);
setIsRenameFolderDialogOpen(true);
}
};
const handleRenameApp = async (renameFolder: boolean) => {
if (!appId || !selectedApp || !newAppName.trim()) return;
try {
setIsRenaming(true);
// Determine the new path based on user's choice
const appPath = renameFolder ? newAppName : selectedApp.path;
await IpcClient.getInstance().renameApp({
appId,
appName: newAppName,
appPath,
});
setIsRenameDialogOpen(false);
setIsRenameConfirmDialogOpen(false);
await refreshApps();
} catch (error) {
console.error("Failed to rename app:", error);
const errorMessage = (
error instanceof Error ? error.message : String(error)
).replace(/^Error invoking remote method 'rename-app': Error: /, "");
showError(errorMessage);
} finally {
setIsRenaming(false);
}
};
const handleRenameFolderOnly = async () => {
if (!appId || !selectedApp || !newFolderName.trim()) return;
try {
setIsRenamingFolder(true);
await IpcClient.getInstance().renameApp({
appId,
appName: selectedApp.name, // Keep the app name the same
appPath: newFolderName, // Change only the folder path
});
setIsRenameFolderDialogOpen(false);
await refreshApps();
} catch (error) {
console.error("Failed to rename folder:", error);
const errorMessage = (
error instanceof Error ? error.message : String(error)
).replace(/^Error invoking remote method 'rename-app': Error: /, "");
showError(errorMessage);
} finally {
setIsRenamingFolder(false);
}
};
const handleAppNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewCopyAppName(e.target.value);
};
const handleOpenCopyDialog = () => {
if (selectedApp) {
setNewCopyAppName(`${selectedApp.name}-copy`);
setIsCopyDialogOpen(true);
}
};
const copyAppMutation = useMutation({
mutationFn: async ({ withHistory }: { withHistory: boolean }) => {
if (!appId || !newCopyAppName.trim()) {
throw new Error("Invalid app ID or name for copying.");
}
return IpcClient.getInstance().copyApp({
appId,
newAppName: newCopyAppName,
withHistory,
});
},
onSuccess: async (data) => {
const appId = data.app.id;
setSelectedAppId(appId);
await invalidateAppQuery(queryClient, { appId });
await refreshApps();
await IpcClient.getInstance().createChat(appId);
setIsCopyDialogOpen(false);
navigate({ to: "/app-details", search: { appId } });
},
onError: (error) => {
showError(error);
},
});
if (!selectedApp) {
return (
<div className="relative min-h-screen p-8">
<Button
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-5"
>
<ArrowLeft className="h-3 w-4" />
Back
</Button>
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-xl font-bold">App not found</h2>
</div>
</div>
);
}
const fullAppPath = appBasePath.replace("$APP_BASE_PATH", selectedApp.path);
return (
<div
className="relative min-h-screen p-4 w-full"
data-testid="app-details-page"
>
<Button
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-2"
>
<ArrowLeft className="h-3 w-4" />
Back
</Button>
<div className="w-full max-w-2xl mx-auto mt-10 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm relative">
<div className="flex items-center mb-3">
<h2 className="text-2xl font-bold">{selectedApp.name}</h2>
<Button
variant="ghost"
size="sm"
className="ml-1 p-0.5 h-auto"
onClick={handleOpenRenameDialog}
data-testid="app-details-rename-app-button"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
</div>
{/* Overflow Menu in top right */}
<div className="absolute top-2 right-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
data-testid="app-details-more-options-button"
>
<MoreVertical className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-40 p-2" align="end">
<div className="flex flex-col space-y-0.5">
<Button
onClick={handleOpenRenameFolderDialog}
variant="ghost"
size="sm"
className="h-8 justify-start text-xs"
>
Rename folder
</Button>
<Button
onClick={handleOpenCopyDialog}
variant="ghost"
size="sm"
className="h-8 justify-start text-xs"
>
Copy app
</Button>
<Button
onClick={() => setIsDeleteDialogOpen(true)}
variant="ghost"
size="sm"
className="h-8 justify-start text-xs"
>
Delete
</Button>
</div>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-3 text-sm mb-4">
<div>
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Created
</span>
<span>{selectedApp.createdAt.toString()}</span>
</div>
<div>
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Last Updated
</span>
<span>{selectedApp.updatedAt.toString()}</span>
</div>
<div className="col-span-2">
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
Path
</span>
<div className="flex items-center gap-1">
<span className="text-sm break-all">{fullAppPath}</span>
<Button
variant="ghost"
size="sm"
className="p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={() => {
IpcClient.getInstance().showItemInFolder(fullAppPath);
}}
title="Show in folder"
>
<Folder className="h-3.5 w-3.5" />
</Button>
</div>
</div>
</div>
<div className="mt-4 flex flex-col gap-2">
<Button
onClick={() => {
if (!appId) {
console.error("No app id found");
return;
}
navigate({ to: "/chat" });
}}
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
size="lg"
>
Open in Chat
<MessageCircle className="h-4 w-4" />
</Button>
<div className="border border-gray-200 rounded-md p-4">
<GitHubConnector appId={appId} folderName={selectedApp.path} />
</div>
{appId && <SupabaseConnector appId={appId} />}
{appId && <CapacitorControls appId={appId} />}
<AppUpgrades appId={appId} />
</div>
{/* Rename Dialog */}
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Rename App</DialogTitle>
</DialogHeader>
<Input
value={newAppName}
onChange={(e) => setNewAppName(e.target.value)}
placeholder="Enter new app name"
className="my-2"
autoFocus
/>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameDialogOpen(false)}
disabled={isRenaming}
size="sm"
>
Cancel
</Button>
<Button
onClick={() => {
setIsRenameDialogOpen(false);
setIsRenameConfirmDialogOpen(true);
}}
disabled={isRenaming || !newAppName.trim()}
size="sm"
>
Continue
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Rename Folder Dialog */}
<Dialog
open={isRenameFolderDialogOpen}
onOpenChange={setIsRenameFolderDialogOpen}
>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Rename app folder</DialogTitle>
<DialogDescription className="text-xs">
This will change only the folder name, not the app name.
</DialogDescription>
</DialogHeader>
<Input
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Enter new folder name"
className="my-2"
autoFocus
/>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameFolderDialogOpen(false)}
disabled={isRenamingFolder}
size="sm"
>
Cancel
</Button>
<Button
onClick={handleRenameFolderOnly}
disabled={isRenamingFolder || !newFolderName.trim()}
size="sm"
>
{isRenamingFolder ? (
<>
<svg
className="animate-spin h-3 w-3 mr-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Renaming...
</>
) : (
"Rename Folder"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Rename Confirmation Dialog */}
<Dialog
open={isRenameConfirmDialogOpen}
onOpenChange={setIsRenameConfirmDialogOpen}
>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle className="text-base">
How would you like to rename "{selectedApp.name}"?
</DialogTitle>
<DialogDescription className="text-xs">
Choose an option:
</DialogDescription>
</DialogHeader>
<div className="space-y-2 my-2">
<Button
variant="outline"
className="w-full justify-start p-2 h-auto relative text-sm"
onClick={() => handleRenameApp(true)}
disabled={isRenaming}
>
<div className="absolute top-1 right-1">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 text-[10px]">
Recommended
</span>
</div>
<div className="text-left">
<p className="font-medium text-xs">Rename app and folder</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Renames the folder to match the new app name.
</p>
</div>
</Button>
<Button
variant="outline"
className="w-full justify-start p-2 h-auto text-sm"
onClick={() => handleRenameApp(false)}
disabled={isRenaming}
>
<div className="text-left">
<p className="font-medium text-xs">Rename app only</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
The folder name will remain the same.
</p>
</div>
</Button>
</div>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsRenameConfirmDialogOpen(false)}
disabled={isRenaming}
size="sm"
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Copy App Dialog */}
{selectedApp && (
<Dialog open={isCopyDialogOpen} onOpenChange={setIsCopyDialogOpen}>
<DialogContent className="max-w-md p-4">
<DialogHeader className="pb-2">
<DialogTitle>Copy "{selectedApp.name}"</DialogTitle>
<DialogDescription className="text-sm">
<p>Create a copy of this app.</p>
<p>
Note: this does not copy over the Supabase project or GitHub
project.
</p>
</DialogDescription>
</DialogHeader>
<div className="space-y-3 my-2">
<div>
<Label htmlFor="newAppName">New app name</Label>
<div className="relative mt-1">
<Input
id="newAppName"
value={newCopyAppName}
onChange={handleAppNameChange}
placeholder="Enter new app name"
className="pr-8"
disabled={copyAppMutation.isPending}
/>
{isCheckingName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
{nameExists && (
<p className="text-xs text-yellow-600 dark:text-yellow-500 mt-1">
An app with this name already exists. Please choose
another name.
</p>
)}
</div>
<div className="space-y-2">
<Button
variant="outline"
className="w-full justify-start p-2 h-auto relative text-sm"
onClick={() =>
copyAppMutation.mutate({ withHistory: true })
}
disabled={
copyAppMutation.isPending ||
nameExists ||
!newCopyAppName.trim() ||
isCheckingName
}
>
{copyAppMutation.isPending &&
copyAppMutation.variables?.withHistory === true && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<div className="absolute top-1 right-1">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 text-[10px]">
Recommended
</span>
</div>
<div className="text-left">
<p className="font-medium text-xs">
Copy app with history
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Copies the entire app, including the Git version
history.
</p>
</div>
</Button>
<Button
variant="outline"
className="w-full justify-start p-2 h-auto text-sm"
onClick={() =>
copyAppMutation.mutate({ withHistory: false })
}
disabled={
copyAppMutation.isPending ||
nameExists ||
!newCopyAppName.trim() ||
isCheckingName
}
>
{copyAppMutation.isPending &&
copyAppMutation.variables?.withHistory === false && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<div className="text-left">
<p className="font-medium text-xs">
Copy app without history
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Useful if the current app has a Git-related issue.
</p>
</div>
</Button>
</div>
</div>
<DialogFooter className="pt-2">
<Button
variant="outline"
onClick={() => setIsCopyDialogOpen(false)}
disabled={copyAppMutation.isPending}
size="sm"
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent className="max-w-sm p-4">
<DialogHeader className="pb-2">
<DialogTitle>Delete "{selectedApp.name}"?</DialogTitle>
<DialogDescription className="text-xs">
This action is irreversible. All app files and chat history will
be permanently deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex justify-end gap-2 pt-2">
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
disabled={isDeleting}
size="sm"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteApp}
disabled={isDeleting}
className="flex items-center gap-1"
size="sm"
>
{isDeleting ? (
<>
<svg
className="animate-spin h-3 w-3 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Deleting...
</>
) : (
"Delete App"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
);
}

View File

@@ -0,0 +1,82 @@
import { useState, useRef, useEffect } from "react";
import {
PanelGroup,
Panel,
PanelResizeHandle,
type ImperativePanelHandle,
} from "react-resizable-panels";
import { ChatPanel } from "../components/ChatPanel";
import { PreviewPanel } from "../components/preview_panel/PreviewPanel";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useChats } from "@/hooks/useChats";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
export default function ChatPage() {
let { id: chatId } = useSearch({ from: "/chat" });
const navigate = useNavigate();
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const [isResizing, setIsResizing] = useState(false);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const { chats, loading } = useChats(selectedAppId);
useEffect(() => {
if (!chatId && chats.length && !loading) {
// Not a real navigation, just a redirect, when the user navigates to /chat
// without a chatId, we redirect to the first chat
setSelectedAppId(chats[0].appId);
navigate({ to: "/chat", search: { id: chats[0].id }, replace: true });
}
}, [chatId, chats, loading, navigate]);
useEffect(() => {
if (isPreviewOpen) {
ref.current?.expand();
} else {
ref.current?.collapse();
}
}, [isPreviewOpen]);
const ref = useRef<ImperativePanelHandle>(null);
return (
<PanelGroup autoSaveId="persistence" direction="horizontal">
<Panel id="chat-panel" minSize={30}>
<div className="h-full w-full">
<ChatPanel
chatId={chatId}
isPreviewOpen={isPreviewOpen}
onTogglePreview={() => {
setIsPreviewOpen(!isPreviewOpen);
if (isPreviewOpen) {
ref.current?.collapse();
} else {
ref.current?.expand();
}
}}
/>
</div>
</Panel>
<>
<PanelResizeHandle
onDragging={(e) => setIsResizing(e)}
className="w-1 bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors cursor-col-resize"
/>
<Panel
collapsible
ref={ref}
id="preview-panel"
minSize={20}
className={cn(
!isResizing && "transition-all duration-100 ease-in-out",
)}
>
<PreviewPanel />
</Panel>
</>
</PanelGroup>
);
}

View File

@@ -0,0 +1,309 @@
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useAtom, useSetAtom } from "jotai";
import { homeChatInputValueAtom } from "../atoms/chatAtoms";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { generateCuteAppName } from "@/lib/utils";
import { useLoadApps } from "@/hooks/useLoadApps";
import { useSettings } from "@/hooks/useSettings";
import { SetupBanner } from "@/components/SetupBanner";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useState, useEffect, useCallback } from "react";
import { useStreamChat } from "@/hooks/useStreamChat";
import { HomeChatInput } from "@/components/chat/HomeChatInput";
import { usePostHog } from "posthog-js/react";
import { PrivacyBanner } from "@/components/TelemetryBanner";
import { INSPIRATION_PROMPTS } from "@/prompts/inspiration_prompts";
import { useAppVersion } from "@/hooks/useAppVersion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useTheme } from "@/contexts/ThemeContext";
import { Button } from "@/components/ui/button";
import { ExternalLink } from "lucide-react";
import { ImportAppButton } from "@/components/ImportAppButton";
import { showError } from "@/lib/toast";
import { invalidateAppQuery } from "@/hooks/useLoadApp";
import { useQueryClient } from "@tanstack/react-query";
import { ForceCloseDialog } from "@/components/ForceCloseDialog";
import type { FileAttachment } from "@/ipc/ipc_types";
import { NEON_TEMPLATE_IDS } from "@/shared/templates";
import { neonTemplateHook } from "@/client_logic/template_hook";
import { ProBanner } from "@/components/ProBanner";
// Adding an export for attachments
export interface HomeSubmitOptions {
attachments?: FileAttachment[];
}
export default function HomePage() {
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
const navigate = useNavigate();
const search = useSearch({ from: "/" });
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const { refreshApps } = useLoadApps();
const { settings, updateSettings } = useSettings();
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [isLoading, setIsLoading] = useState(false);
const [forceCloseDialogOpen, setForceCloseDialogOpen] = useState(false);
const [performanceData, setPerformanceData] = useState<any>(undefined);
const { streamMessage } = useStreamChat({ hasChatId: false });
const posthog = usePostHog();
const appVersion = useAppVersion();
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
const [releaseUrl, setReleaseUrl] = useState("");
const { theme } = useTheme();
const queryClient = useQueryClient();
// Listen for force-close events
useEffect(() => {
const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onForceCloseDetected((data) => {
setPerformanceData(data.performanceData);
setForceCloseDialogOpen(true);
});
return () => unsubscribe();
}, []);
useEffect(() => {
const updateLastVersionLaunched = async () => {
if (
appVersion &&
settings &&
settings.lastShownReleaseNotesVersion !== appVersion
) {
const shouldShowReleaseNotes = !!settings.lastShownReleaseNotesVersion;
await updateSettings({
lastShownReleaseNotesVersion: appVersion,
});
// It feels spammy to show release notes if it's
// the users very first time.
if (!shouldShowReleaseNotes) {
return;
}
try {
const result = await IpcClient.getInstance().doesReleaseNoteExist({
version: appVersion,
});
if (result.exists && result.url) {
setReleaseUrl(result.url + "?hideHeader=true&theme=" + theme);
setReleaseNotesOpen(true);
}
} catch (err) {
console.warn(
"Unable to check if release note exists for: " + appVersion,
err,
);
}
}
};
updateLastVersionLaunched();
}, [appVersion, settings, updateSettings, theme]);
// Get the appId from search params
const appId = search.appId ? Number(search.appId) : null;
// State for random prompts
const [randomPrompts, setRandomPrompts] = useState<
typeof INSPIRATION_PROMPTS
>([]);
// Function to get random prompts
const getRandomPrompts = useCallback(() => {
const shuffled = [...INSPIRATION_PROMPTS].sort(() => 0.5 - Math.random());
return shuffled.slice(0, 3);
}, []);
// Initialize random prompts
useEffect(() => {
setRandomPrompts(getRandomPrompts());
}, [getRandomPrompts]);
// Redirect to app details page if appId is present
useEffect(() => {
if (appId) {
navigate({ to: "/app-details", search: { appId } });
}
}, [appId, navigate]);
const handleSubmit = async (options?: HomeSubmitOptions) => {
const attachments = options?.attachments || [];
if (!inputValue.trim() && attachments.length === 0) return;
try {
setIsLoading(true);
// Create the chat and navigate
const result = await IpcClient.getInstance().createApp({
name: generateCuteAppName(),
});
if (
settings?.selectedTemplateId &&
NEON_TEMPLATE_IDS.has(settings.selectedTemplateId)
) {
await neonTemplateHook({
appId: result.app.id,
appName: result.app.name,
});
}
// Stream the message with attachments
streamMessage({
prompt: inputValue,
chatId: result.chatId,
attachments,
});
await new Promise((resolve) =>
setTimeout(resolve, settings?.isTestMode ? 0 : 2000),
);
setInputValue("");
setSelectedAppId(result.app.id);
setIsPreviewOpen(false);
await refreshApps(); // Ensure refreshApps is awaited if it's async
await invalidateAppQuery(queryClient, { appId: result.app.id });
posthog.capture("home:chat-submit");
navigate({ to: "/chat", search: { id: result.chatId } });
} catch (error) {
console.error("Failed to create chat:", error);
showError("Failed to create app. " + (error as any).toString());
setIsLoading(false); // Ensure loading state is reset on error
}
// No finally block needed for setIsLoading(false) here if navigation happens on success
};
// Loading overlay for app creation
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center max-w-3xl m-auto p-8">
<div className="w-full flex flex-col items-center">
{/* Loading Spinner */}
<div className="relative w-24 h-24 mb-8">
<div className="absolute top-0 left-0 w-full h-full border-8 border-gray-200 dark:border-gray-700 rounded-full"></div>
<div className="absolute top-0 left-0 w-full h-full border-8 border-t-primary rounded-full animate-spin"></div>
</div>
<h2 className="text-2xl font-bold mb-2 text-gray-800 dark:text-gray-200">
Building your app
</h2>
<p className="text-gray-600 dark:text-gray-400 text-center max-w-md mb-8">
We're setting up your app with AI magic. <br />
This might take a moment...
</p>
</div>
</div>
);
}
// Main Home Page Content
return (
<div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8">
<ForceCloseDialog
isOpen={forceCloseDialogOpen}
onClose={() => setForceCloseDialogOpen(false)}
performanceData={performanceData}
/>
<SetupBanner />
<div className="w-full">
<ImportAppButton />
<HomeChatInput onSubmit={handleSubmit} />
<div className="flex flex-col gap-4 mt-2">
<div className="flex flex-wrap gap-4 justify-center">
{randomPrompts.map((item, index) => (
<button
type="button"
key={index}
onClick={() => setInputValue(`Build me a ${item.label}`)}
className="flex items-center gap-3 px-4 py-2 rounded-xl border border-gray-200
bg-white/50 backdrop-blur-sm
transition-all duration-200
hover:bg-white hover:shadow-md hover:border-gray-300
active:scale-[0.98]
dark:bg-gray-800/50 dark:border-gray-700
dark:hover:bg-gray-800 dark:hover:border-gray-600"
>
<span className="text-gray-700 dark:text-gray-300">
{item.icon}
</span>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{item.label}
</span>
</button>
))}
</div>
<button
type="button"
onClick={() => setRandomPrompts(getRandomPrompts())}
className="self-center flex items-center gap-2 px-4 py-2 rounded-xl border border-gray-200
bg-white/50 backdrop-blur-sm
transition-all duration-200
hover:bg-white hover:shadow-md hover:border-gray-300
active:scale-[0.98]
dark:bg-gray-800/50 dark:border-gray-700
dark:hover:bg-gray-800 dark:hover:border-gray-600"
>
<svg
className="w-5 h-5 text-gray-700 dark:text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
More ideas
</span>
</button>
</div>
<ProBanner />
</div>
<PrivacyBanner />
{/* Release Notes Dialog */}
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
<DialogContent className="max-w-4xl bg-(--docs-bg) pr-0 pt-4 pl-4 gap-1">
<DialogHeader>
<DialogTitle>What's new in v{appVersion}?</DialogTitle>
<Button
variant="ghost"
size="sm"
className="absolute right-10 top-2 focus-visible:ring-0 focus-visible:ring-offset-0"
onClick={() =>
window.open(
releaseUrl.replace("?hideHeader=true&theme=" + theme, ""),
"_blank",
)
}
>
<ExternalLink className="w-4 h-4" />
</Button>
</DialogHeader>
<div className="overflow-auto h-[70vh] flex flex-col ">
{releaseUrl && (
<div className="flex-1">
<iframe
src={releaseUrl}
className="w-full h-full border-0 rounded-lg"
title={`Release notes for v${appVersion}`}
/>
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,124 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "@tanstack/react-router";
import { useSettings } from "@/hooks/useSettings";
import { useTemplates } from "@/hooks/useTemplates";
import { TemplateCard } from "@/components/TemplateCard";
import { CreateAppDialog } from "@/components/CreateAppDialog";
import { NeonConnector } from "@/components/NeonConnector";
const HubPage: React.FC = () => {
const router = useRouter();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const { templates, isLoading } = useTemplates();
const { settings, updateSettings } = useSettings();
const selectedTemplateId = settings?.selectedTemplateId;
const handleTemplateSelect = (templateId: string) => {
updateSettings({ selectedTemplateId: templateId });
};
const handleCreateApp = () => {
setIsCreateDialogOpen(true);
};
// Separate templates into official and community
const officialTemplates =
templates?.filter((template) => template.isOfficial) || [];
const communityTemplates =
templates?.filter((template) => !template.isOfficial) || [];
return (
<div className="min-h-screen px-8 py-4">
<div className="max-w-5xl mx-auto pb-12">
<Button
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</Button>
<header className="mb-8 text-left">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Pick your default template
</h1>
<p className="text-md text-gray-600 dark:text-gray-400">
Choose a starting point for your new project.
{isLoading && " Loading additional templates..."}
</p>
</header>
{/* Official Templates Section */}
{officialTemplates.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Official templates
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{officialTemplates.map((template) => (
<TemplateCard
key={template.id}
template={template}
isSelected={template.id === selectedTemplateId}
onSelect={handleTemplateSelect}
onCreateApp={handleCreateApp}
/>
))}
</div>
</section>
)}
{/* Community Templates Section */}
{communityTemplates.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Community templates
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{communityTemplates.map((template) => (
<TemplateCard
key={template.id}
template={template}
isSelected={template.id === selectedTemplateId}
onSelect={handleTemplateSelect}
onCreateApp={handleCreateApp}
/>
))}
</div>
</section>
)}
<BackendSection />
</div>
<CreateAppDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
template={templates.find((t) => t.id === settings?.selectedTemplateId)}
/>
</div>
);
};
function BackendSection() {
return (
<div className="">
<header className="mb-4 text-left">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Backend Services
</h1>
<p className="text-md text-gray-600 dark:text-gray-400">
Connect to backend services for your projects.
</p>
</header>
<div className="grid grid-cols-1 gap-6">
<NeonConnector />
</div>
</div>
);
}
export default HubPage;

View File

@@ -0,0 +1,141 @@
import React, { useState, useEffect } from "react";
import { usePrompts } from "@/hooks/usePrompts";
import {
CreatePromptDialog,
CreateOrEditPromptDialog,
} from "@/components/CreatePromptDialog";
import { DeleteConfirmationDialog } from "@/components/DeleteConfirmationDialog";
import { useDeepLink } from "@/contexts/DeepLinkContext";
import { AddPromptDeepLinkData } from "@/ipc/deep_link_data";
import { showInfo } from "@/lib/toast";
export default function LibraryPage() {
const { prompts, isLoading, createPrompt, updatePrompt, deletePrompt } =
usePrompts();
const { lastDeepLink, clearLastDeepLink } = useDeepLink();
const [dialogOpen, setDialogOpen] = useState(false);
const [prefillData, setPrefillData] = useState<
| {
title: string;
description: string;
content: string;
}
| undefined
>(undefined);
useEffect(() => {
const handleDeepLink = async () => {
if (lastDeepLink?.type === "add-prompt") {
const deepLink = lastDeepLink as AddPromptDeepLinkData;
const payload = deepLink.payload;
showInfo(`Prefilled prompt: ${payload.title}`);
setPrefillData({
title: payload.title,
description: payload.description,
content: payload.content,
});
setDialogOpen(true);
clearLastDeepLink();
}
};
handleDeepLink();
}, [lastDeepLink?.timestamp, clearLastDeepLink]);
const handleDialogClose = (open: boolean) => {
setDialogOpen(open);
if (!open) {
// Clear prefill data when dialog closes
setPrefillData(undefined);
}
};
return (
<div className="min-h-screen px-8 py-6">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold mr-4">Library: Prompts</h1>
<CreatePromptDialog
onCreatePrompt={createPrompt}
prefillData={prefillData}
isOpen={dialogOpen}
onOpenChange={handleDialogClose}
/>
</div>
{isLoading ? (
<div>Loading...</div>
) : prompts.length === 0 ? (
<div className="text-muted-foreground">
No prompts yet. Create one to get started.
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
{prompts.map((p) => (
<PromptCard
key={p.id}
prompt={p}
onUpdate={updatePrompt}
onDelete={deletePrompt}
/>
))}
</div>
)}
</div>
</div>
);
}
function PromptCard({
prompt,
onUpdate,
onDelete,
}: {
prompt: {
id: number;
title: string;
description: string | null;
content: string;
};
onUpdate: (p: {
id: number;
title: string;
description?: string;
content: string;
}) => Promise<void>;
onDelete: (id: number) => Promise<void>;
}) {
return (
<div
data-testid="prompt-card"
className="border rounded-lg p-4 bg-(--background-lightest) min-w-80"
>
<div className="space-y-2">
<div className="flex items-start justify-between">
<div>
<h3 className="text-lg font-semibold">{prompt.title}</h3>
{prompt.description && (
<p className="text-sm text-muted-foreground">
{prompt.description}
</p>
)}
</div>
<div className="flex gap-2">
<CreateOrEditPromptDialog
mode="edit"
prompt={prompt}
onUpdatePrompt={onUpdate}
/>
<DeleteConfirmationDialog
itemName={prompt.title}
itemType="Prompt"
onDelete={() => onDelete(prompt.id)}
/>
</div>
</div>
<pre className="text-sm whitespace-pre-wrap bg-transparent border rounded p-2 max-h-48 overflow-auto">
{prompt.content}
</pre>
</div>
</div>
);
}

View File

@@ -0,0 +1,337 @@
import { useEffect, useState } from "react";
import { useTheme } from "../contexts/ThemeContext";
import { ProviderSettingsGrid } from "@/components/ProviderSettings";
import ConfirmationDialog from "@/components/ConfirmationDialog";
import { IpcClient } from "@/ipc/ipc_client";
import { showSuccess, showError } from "@/lib/toast";
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
import { MaxChatTurnsSelector } from "@/components/MaxChatTurnsSelector";
import { ThinkingBudgetSelector } from "@/components/ThinkingBudgetSelector";
import { useSettings } from "@/hooks/useSettings";
import { useAppVersion } from "@/hooks/useAppVersion";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "@tanstack/react-router";
import { GitHubIntegration } from "@/components/GitHubIntegration";
import { VercelIntegration } from "@/components/VercelIntegration";
import { SupabaseIntegration } from "@/components/SupabaseIntegration";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch";
import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector";
import { NeonIntegration } from "@/components/NeonIntegration";
import { RuntimeModeSelector } from "@/components/RuntimeModeSelector";
import { NodePathSelector } from "@/components/NodePathSelector";
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
import { ZoomSelector } from "@/components/ZoomSelector";
import { useSetAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
export default function SettingsPage() {
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const appVersion = useAppVersion();
const { settings, updateSettings } = useSettings();
const router = useRouter();
const setActiveSettingsSection = useSetAtom(activeSettingsSectionAtom);
useEffect(() => {
setActiveSettingsSection("general-settings");
}, [setActiveSettingsSection]);
const handleResetEverything = async () => {
setIsResetting(true);
try {
const ipcClient = IpcClient.getInstance();
await ipcClient.resetAll();
showSuccess("Successfully reset everything. Restart the application.");
} catch (error) {
console.error("Error resetting:", error);
showError(
error instanceof Error ? error.message : "An unknown error occurred",
);
} finally {
setIsResetting(false);
setIsResetDialogOpen(false);
}
};
return (
<div className="min-h-screen px-8 py-4">
<div className="max-w-5xl mx-auto">
<Button
onClick={() => router.history.back()}
variant="outline"
size="sm"
className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</Button>
<div className="flex justify-between mb-4">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Settings
</h1>
</div>
<div className="space-y-6">
<GeneralSettings appVersion={appVersion} />
<WorkflowSettings />
<AISettings />
<div
id="provider-settings"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm"
>
<ProviderSettingsGrid />
</div>
<div className="space-y-6">
<div
id="telemetry"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Telemetry
</h2>
<div className="space-y-2">
<TelemetrySwitch />
<div className="text-sm text-gray-500 dark:text-gray-400">
This records anonymous usage data to improve the product.
</div>
</div>
<div className="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400">
<span className="mr-2 font-medium">Telemetry ID:</span>
<span className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono">
{settings ? settings.telemetryUserId : "n/a"}
</span>
</div>
</div>
</div>
{/* Integrations Section */}
<div
id="integrations"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Integrations
</h2>
<div className="space-y-4">
<GitHubIntegration />
<VercelIntegration />
<SupabaseIntegration />
<NeonIntegration />
</div>
</div>
{/* Tools (MCP) */}
<div
id="tools-mcp"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Tools (MCP)
</h2>
<ToolsMcpSettings />
</div>
{/* Experiments Section */}
<div
id="experiments"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Experiments
</h2>
<div className="space-y-4">
<div className="space-y-1 mt-4">
<div className="flex items-center space-x-2">
<Switch
id="enable-native-git"
checked={!!settings?.enableNativeGit}
onCheckedChange={(checked) => {
updateSettings({
enableNativeGit: checked,
});
}}
/>
<Label htmlFor="enable-native-git">Enable Native Git</Label>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
This doesn't require any external Git installation and offers
a faster, native-Git performance experience.
</div>
</div>
</div>
</div>
{/* Danger Zone */}
<div
id="danger-zone"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-red-200 dark:border-red-800"
>
<h2 className="text-lg font-medium text-red-600 dark:text-red-400 mb-4">
Danger Zone
</h2>
<div className="space-y-4">
<div className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4">
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
Reset Everything
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
This will delete all your apps, chats, and settings. This
action cannot be undone.
</p>
</div>
<button
onClick={() => setIsResetDialogOpen(true)}
disabled={isResetting}
className="rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isResetting ? "Resetting..." : "Reset Everything"}
</button>
</div>
</div>
</div>
</div>
</div>
<ConfirmationDialog
isOpen={isResetDialogOpen}
title="Reset Everything"
message="Are you sure you want to reset everything? This will delete all your apps, chats, and settings. This action cannot be undone."
confirmText="Reset Everything"
cancelText="Cancel"
onConfirm={handleResetEverything}
onCancel={() => setIsResetDialogOpen(false)}
/>
</div>
);
}
export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
const { theme, setTheme } = useTheme();
return (
<div
id="general-settings"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
General Settings
</h2>
<div className="space-y-4 mb-4">
<div className="flex items-center gap-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Theme
</label>
<div className="relative bg-gray-100 dark:bg-gray-700 rounded-lg p-1 flex">
{(["system", "light", "dark"] as const).map((option) => (
<button
key={option}
onClick={() => setTheme(option)}
className={`
px-4 py-1.5 text-sm font-medium rounded-md
transition-all duration-200
${
theme === option
? "bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
}
`}
>
{option.charAt(0).toUpperCase() + option.slice(1)}
</button>
))}
</div>
</div>
</div>
<div className="mt-4">
<ZoomSelector />
</div>
<div className="space-y-1 mt-4">
<AutoUpdateSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically update the app when new versions are
available.
</div>
</div>
<div className="mt-4">
<ReleaseChannelSelector />
</div>
<div className="mt-4">
<RuntimeModeSelector />
</div>
<div className="mt-4">
<NodePathSelector />
</div>
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400 mt-4">
<span className="mr-2 font-medium">App Version:</span>
<span className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono">
{appVersion ? appVersion : "-"}
</span>
</div>
</div>
);
}
export function WorkflowSettings() {
return (
<div
id="workflow-settings"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Workflow Settings
</h2>
<div className="space-y-1">
<AutoApproveSwitch showToast={false} />
<div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically approve code changes and run them.
</div>
</div>
<div className="space-y-1 mt-4">
<AutoFixProblemsSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically fix TypeScript errors.
</div>
</div>
</div>
);
}
export function AISettings() {
return (
<div
id="ai-settings"
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
>
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
AI Settings
</h2>
<div className="mt-4">
<ThinkingBudgetSelector />
</div>
<div className="mt-4">
<MaxChatTurnsSelector />
</div>
</div>
);
}