Disable auto-update setting & settings page has scroll shortcuts (#590)

Fixes https://github.com/dyad-sh/dyad/issues/561
This commit is contained in:
Will Chen
2025-07-07 15:43:06 -07:00
committed by GitHub
parent bc38f9b2d7
commit ab6a9d3b34
28 changed files with 526 additions and 136 deletions

View File

@@ -0,0 +1,15 @@
import { expect } from "@playwright/test";
import { test } from "./helpers/test_helper";
test("auto update - disable and enable", async ({ po }) => {
await po.goToSettingsTab();
await po.toggleAutoUpdate();
await expect(
po.page.getByRole("button", { name: "Restart Dyad" }),
).toBeVisible();
await po.snapshotSettings();
await po.toggleAutoUpdate();
await po.snapshotSettings();
});

View File

@@ -833,6 +833,10 @@ export class PageObject {
expect(sanitizedSettingsContent).toMatchSnapshot(); expect(sanitizedSettingsContent).toMatchSnapshot();
} }
async toggleAutoUpdate() {
await this.page.getByRole("switch", { name: "Auto-update" }).click();
}
async clickTelemetryAccept() { async clickTelemetryAccept() {
await this.page.getByTestId("telemetry-accept-button").click(); await this.page.getByTestId("telemetry-accept-button").click();
} }

View File

@@ -0,0 +1,18 @@
{
"selectedModel": {
"name": "auto",
"provider": "auto"
},
"providerSettings": {},
"telemetryConsent": "unset",
"telemetryUserId": "[UUID]",
"hasRunBefore": true,
"experiments": {},
"lastShownReleaseNotesVersion": "[scrubbed]",
"enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true,
"selectedChatMode": "build",
"enableAutoFixProblems": false,
"enableAutoUpdate": false,
"isTestMode": true
}

View File

@@ -0,0 +1,18 @@
{
"selectedModel": {
"name": "auto",
"provider": "auto"
},
"providerSettings": {},
"telemetryConsent": "unset",
"telemetryUserId": "[UUID]",
"hasRunBefore": true,
"experiments": {},
"lastShownReleaseNotesVersion": "[scrubbed]",
"enableProLazyEditsMode": true,
"enableProSmartFilesContextMode": true,
"selectedChatMode": "build",
"enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true
}

View File

@@ -15,5 +15,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -12,5 +12,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -13,5 +13,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -12,5 +12,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -13,5 +13,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -12,5 +12,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -13,5 +13,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -22,5 +22,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -22,5 +22,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -22,5 +22,6 @@
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"selectedChatMode": "build", "selectedChatMode": "build",
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true,
"isTestMode": true "isTestMode": true
} }

77
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "dyad", "name": "dyad",
"version": "0.8.0", "version": "0.11.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.8.0", "version": "0.11.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",
@@ -24,6 +24,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.2", "@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
@@ -4680,6 +4681,78 @@
} }
} }
}, },
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
"integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": { "node_modules/@radix-ui/react-select": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz",

View File

@@ -97,6 +97,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.2", "@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",

View File

@@ -0,0 +1,36 @@
import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { toast } from "sonner";
import { IpcClient } from "@/ipc/ipc_client";
export function AutoUpdateSwitch() {
const { settings, updateSettings } = useSettings();
if (!settings) {
return null;
}
return (
<div className="flex items-center space-x-2">
<Switch
id="enable-auto-update"
checked={settings.enableAutoUpdate}
onCheckedChange={(checked) => {
updateSettings({ enableAutoUpdate: checked });
toast("Auto-update settings changed", {
description:
"You will need to restart Dyad for your settings to take effect.",
action: {
label: "Restart Dyad",
onClick: () => {
IpcClient.getInstance().restartDyad();
},
},
});
}}
/>
<Label htmlFor="enable-auto-update">Auto-update</Label>
</div>
);
}

View File

@@ -68,7 +68,7 @@ export function ProviderSettingsGrid() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-2xl font-bold mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">AI Providers</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5].map((i) => ( {[1, 2, 3, 4, 5].map((i) => (
<Card key={i} className="border-border"> <Card key={i} className="border-border">
@@ -86,7 +86,7 @@ export function ProviderSettingsGrid() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-2xl font-bold mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">AI Providers</h2>
<Alert variant="destructive"> <Alert variant="destructive">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle> <AlertTitle>Error</AlertTitle>
@@ -100,7 +100,7 @@ export function ProviderSettingsGrid() {
return ( return (
<div className="p-6"> <div className="p-6">
<h2 className="text-2xl font-bold mb-6">AI Providers</h2> <h2 className="text-lg font-medium mb-6">AI Providers</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{providers {providers
?.filter((p) => p.type !== "local") ?.filter((p) => p.type !== "local")
@@ -116,7 +116,7 @@ export function ProviderSettingsGrid() {
className="p-4 cursor-pointer" className="p-4 cursor-pointer"
onClick={() => handleProviderClick(provider.id)} onClick={() => handleProviderClick(provider.id)}
> >
<CardTitle className="text-xl flex items-center justify-between"> <CardTitle className="text-lg font-medium flex items-center justify-between">
{provider.name} {provider.name}
{isProviderSetup(provider.id) ? ( {isProviderSetup(provider.id) ? (
<span className="ml-3 text-sm font-medium text-green-500 bg-green-50 dark:bg-green-900/30 border border-green-500/50 dark:border-green-500/50 px-2 py-1 rounded-full"> <span className="ml-3 text-sm font-medium text-green-500 bg-green-50 dark:bg-green-900/30 border border-green-500/50 dark:border-green-500/50 px-2 py-1 rounded-full">
@@ -178,8 +178,8 @@ export function ProviderSettingsGrid() {
onClick={() => setIsDialogOpen(true)} onClick={() => setIsDialogOpen(true)}
> >
<CardHeader className="p-4 flex flex-col items-center justify-center h-full"> <CardHeader className="p-4 flex flex-col items-center justify-center h-full">
<PlusIcon className="h-10 w-10 text-muted-foreground mb-2" /> <PlusIcon className="h-8 w-8 text-muted-foreground mb-2" />
<CardTitle className="text-xl text-center"> <CardTitle className="text-lg font-medium text-center">
Add custom provider Add custom provider
</CardTitle> </CardTitle>
<CardDescription className="text-center"> <CardDescription className="text-center">

View File

@@ -0,0 +1,88 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
const SETTINGS_SECTIONS = [
{ id: "general-settings", label: "General" },
{ id: "workflow-settings", label: "Workflow" },
{ id: "ai-settings", label: "AI" },
{ id: "provider-settings", label: "Model Providers" },
{ id: "telemetry", label: "Telemetry" },
{ id: "integrations", label: "Integrations" },
{ id: "experiments", label: "Experiments" },
{ id: "danger-zone", label: "Danger Zone" },
];
export function SettingsList({ show }: { show: boolean }) {
const navigate = useNavigate();
const [activeSection, setActiveSection] = useState<string | null>(
"general-settings",
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
return;
}
}
},
{ rootMargin: "-20% 0px -80% 0px", threshold: 0 },
);
for (const section of SETTINGS_SECTIONS) {
const el = document.getElementById(section.id);
if (el) {
observer.observe(el);
}
}
return () => {
observer.disconnect();
};
}, []);
if (!show) {
return null;
}
const handleScrollAndNavigateTo = async (id: string) => {
await navigate({
to: "/settings",
});
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
setActiveSection(id);
}
};
return (
<div className="flex flex-col h-full">
<div className="flex-shrink-0 p-4">
<h2 className="text-lg font-semibold tracking-tight">Settings</h2>
</div>
<ScrollArea className="flex-grow">
<div className="space-y-1 p-4 pt-0">
{SETTINGS_SECTIONS.map((section) => (
<button
key={section.id}
onClick={() => handleScrollAndNavigateTo(section.id)}
className={cn(
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
activeSection === section.id
? "bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
: "hover:bg-sidebar-accent",
)}
>
{section.label}
</button>
))}
</div>
</ScrollArea>
</div>
);
}

View File

@@ -20,6 +20,7 @@ import {
import { ChatList } from "./ChatList"; import { ChatList } from "./ChatList";
import { AppList } from "./AppList"; import { AppList } from "./AppList";
import { HelpDialog } from "./HelpDialog"; // Import the new dialog import { HelpDialog } from "./HelpDialog"; // Import the new dialog
import { SettingsList } from "./SettingsList";
// Menu items. // Menu items.
const items = [ const items = [
@@ -49,6 +50,7 @@ const items = [
type HoverState = type HoverState =
| "start-hover:app" | "start-hover:app"
| "start-hover:chat" | "start-hover:chat"
| "start-hover:settings"
| "clear-hover" | "clear-hover"
| "no-hover"; | "no-hover";
@@ -60,10 +62,7 @@ export function AppSidebar() {
const [isDropdownOpen] = useAtom(dropdownOpenAtom); const [isDropdownOpen] = useAtom(dropdownOpenAtom);
useEffect(() => { useEffect(() => {
if ( if (hoverState.startsWith("start-hover") && state === "collapsed") {
(hoverState === "start-hover:app" || hoverState === "start-hover:chat") &&
state === "collapsed"
) {
expandedByHover.current = true; expandedByHover.current = true;
toggleSidebar(); toggleSidebar();
} }
@@ -84,17 +83,22 @@ export function AppSidebar() {
routerState.location.pathname === "/" || routerState.location.pathname === "/" ||
routerState.location.pathname.startsWith("/app-details"); routerState.location.pathname.startsWith("/app-details");
const isChatRoute = routerState.location.pathname === "/chat"; const isChatRoute = routerState.location.pathname === "/chat";
const isSettingsRoute = routerState.location.pathname.startsWith("/settings");
let selectedItem: string | null = null; let selectedItem: string | null = null;
if (hoverState === "start-hover:app") { if (hoverState === "start-hover:app") {
selectedItem = "Apps"; selectedItem = "Apps";
} else if (hoverState === "start-hover:chat") { } else if (hoverState === "start-hover:chat") {
selectedItem = "Chat"; selectedItem = "Chat";
} else if (hoverState === "start-hover:settings") {
selectedItem = "Settings";
} else if (state === "expanded") { } else if (state === "expanded") {
if (isAppRoute) { if (isAppRoute) {
selectedItem = "Apps"; selectedItem = "Apps";
} else if (isChatRoute) { } else if (isChatRoute) {
selectedItem = "Chat"; selectedItem = "Chat";
} else if (isSettingsRoute) {
selectedItem = "Settings";
} }
} }
@@ -122,6 +126,7 @@ export function AppSidebar() {
<div className="w-[240px]"> <div className="w-[240px]">
<AppList show={selectedItem === "Apps"} /> <AppList show={selectedItem === "Apps"} />
<ChatList show={selectedItem === "Chat"} /> <ChatList show={selectedItem === "Chat"} />
<SettingsList show={selectedItem === "Settings"} />
</div> </div>
</div> </div>
</SidebarContent> </SidebarContent>
@@ -188,6 +193,8 @@ function AppIcons({
onHoverChange("start-hover:app"); onHoverChange("start-hover:app");
} else if (item.title === "Chat") { } else if (item.title === "Chat") {
onHoverChange("start-hover:chat"); onHoverChange("start-hover:chat");
} else if (item.title === "Settings") {
onHoverChange("start-hover:settings");
} }
}} }}
> >

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@@ -1,4 +1,4 @@
import { ipcMain } from "electron"; import { ipcMain, app } from "electron";
import { db, getDatabasePath } from "../../db"; import { db, getDatabasePath } from "../../db";
import { apps, chats } from "../../db/schema"; import { apps, chats } from "../../db/schema";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
@@ -186,6 +186,11 @@ async function killProcessOnPort(port: number): Promise<void> {
} }
export function registerAppHandlers() { export function registerAppHandlers() {
handle("restart-dyad", async () => {
app.relaunch();
app.quit();
});
handle( handle(
"create-app", "create-app",
async ( async (

View File

@@ -165,6 +165,10 @@ export class IpcClient {
return IpcClient.instance; return IpcClient.instance;
} }
public async restartDyad(): Promise<void> {
await this.ipcRenderer.invoke("restart-dyad");
}
public async reloadEnvPath(): Promise<void> { public async reloadEnvPath(): Promise<void> {
await this.ipcRenderer.invoke("reload-env-path"); await this.ipcRenderer.invoke("reload-env-path");
} }

View File

@@ -151,6 +151,7 @@ export const UserSettingsSchema = z.object({
enableAutoFixProblems: z.boolean().optional(), enableAutoFixProblems: z.boolean().optional(),
enableNativeGit: z.boolean().optional(), enableNativeGit: z.boolean().optional(),
enableAutoUpdate: z.boolean(),
//////////////////////////////// ////////////////////////////////
// E2E TESTING ONLY. // E2E TESTING ONLY.

View File

@@ -17,7 +17,11 @@ log.scope.labelPadding = false;
const logger = log.scope("main"); const logger = log.scope("main");
updateElectronApp(); // additional configuration options available // Check settings before enabling auto-update
const settings = readSettings();
if (settings.enableAutoUpdate) {
updateElectronApp({ logger }); // additional configuration options available
}
// Load environment variables from .env file // Load environment variables from .env file
dotenv.config(); dotenv.config();

View File

@@ -21,6 +21,7 @@ const DEFAULT_SETTINGS: UserSettings = {
enableProSmartFilesContextMode: true, enableProSmartFilesContextMode: true,
selectedChatMode: "build", selectedChatMode: "build",
enableAutoFixProblems: false, enableAutoFixProblems: false,
enableAutoUpdate: true,
}; };
const SETTINGS_FILE = "user-settings.json"; const SETTINGS_FILE = "user-settings.json";

View File

@@ -18,9 +18,9 @@ import { SupabaseIntegration } from "@/components/SupabaseIntegration";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch"; import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch";
export default function SettingsPage() { export default function SettingsPage() {
const { theme, setTheme } = useTheme();
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
const appVersion = useAppVersion(); const appVersion = useAppVersion();
@@ -60,108 +60,25 @@ export default function SettingsPage() {
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Settings Settings
</h1> </h1>
{/* App Version Section */}
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
<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> </div>
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"> <GeneralSettings appVersion={appVersion} />
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <WorkflowSettings />
General Settings <AISettings />
</h2>
<div className="space-y-4 mb-4"> <div
<div className="flex items-center gap-4"> id="provider-settings"
<label className="text-sm font-medium text-gray-700 dark:text-gray-300"> className="bg-white dark:bg-gray-800 rounded-xl shadow-sm"
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="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 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">
(Experimental) Native Git offers faster performance but requires{" "}
<a
onClick={() => {
IpcClient.getInstance().openExternalUrl(
"https://git-scm.com/downloads",
);
}}
className="text-blue-600 hover:underline dark:text-blue-400"
>
installing Git
</a>
.
</div>
</div>
<div className="mt-4">
<ThinkingBudgetSelector />
</div>
<div className="mt-4">
<MaxChatTurnsSelector />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<ProviderSettingsGrid /> <ProviderSettingsGrid />
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-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"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Telemetry Telemetry
</h2> </h2>
@@ -182,7 +99,10 @@ export default function SettingsPage() {
</div> </div>
{/* Integrations Section */} {/* Integrations Section */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"> <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"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Integrations Integrations
</h2> </h2>
@@ -193,41 +113,74 @@ export default function SettingsPage() {
</div> </div>
{/* Experiments Section */} {/* Experiments Section */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"> <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"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Experiments Experiments
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
{/* Enable File Editing Experiment */} <div className="space-y-1 mt-4">
<div className="flex items-center justify-between"> <div className="flex items-center space-x-2">
<label <Switch
htmlFor="enable-file-editing" id="enable-native-git"
className="text-sm font-medium text-gray-700 dark:text-gray-300" checked={!!settings?.enableNativeGit}
> onCheckedChange={(checked) => {
Enable File Editing updateSettings({
</label> enableNativeGit: checked,
<Switch });
id="enable-file-editing" }}
checked={!!settings?.experiments?.enableFileEditing} />
onCheckedChange={(checked) => { <Label htmlFor="enable-native-git">Enable Native Git</Label>
updateSettings({ </div>
experiments: { <div className="text-sm text-gray-500 dark:text-gray-400">
...settings?.experiments, Native Git offers faster performance but requires{" "}
enableFileEditing: checked, <a
}, onClick={() => {
}); IpcClient.getInstance().openExternalUrl(
}} "https://git-scm.com/downloads",
/> );
}}
className="text-blue-600 hover:underline dark:text-blue-400"
>
installing Git
</a>
.
</div>
</div>
{/* Enable File Editing Experiment */}
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Switch
id="enable-file-editing"
checked={!!settings?.experiments?.enableFileEditing}
onCheckedChange={(checked) =>
updateSettings({
experiments: {
...settings?.experiments,
enableFileEditing: checked,
},
})
}
/>
<Label htmlFor="enable-file-editing">
Enable File Editing
</Label>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
File editing is not reliable and requires you to manually
commit changes and update Supabase edge functions.
</div>
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
File editing is not reliable and requires you to manually commit
changes and update Supabase edge functions.
</p>
</div> </div>
</div> </div>
{/* Danger Zone */} {/* Danger Zone */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-red-200 dark:border-red-800"> <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"> <h2 className="text-lg font-medium text-red-600 dark:text-red-400 mb-4">
Danger Zone Danger Zone
</h2> </h2>
@@ -268,3 +221,108 @@ export default function SettingsPage() {
</div> </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="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="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>
);
}

View File

@@ -89,6 +89,7 @@ const validInvokeChannels = [
"open-ios", "open-ios",
"open-android", "open-android",
"check-problems", "check-problems",
"restart-dyad",
// Test-only channels // Test-only channels
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process. // These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
// We can't detect with IS_TEST_BUILD in the preload script because // We can't detect with IS_TEST_BUILD in the preload script because