Support native Git (experimental) (#338)

This commit is contained in:
Will Chen
2025-06-04 21:37:05 -07:00
committed by GitHub
parent 4e38031a65
commit 3558663ab7
25 changed files with 455 additions and 96 deletions

View File

@@ -20,9 +20,7 @@ test("delete app", async ({ po }) => {
await po.page.getByRole("button", { name: "Delete App" }).click(); await po.page.getByRole("button", { name: "Delete App" }).click();
// Make sure the app is deleted // Make sure the app is deleted
await expect(async () => { await po.isCurrentAppNameNone();
expect(await po.getCurrentAppName()).toBe("(no app selected)");
}).toPass();
expect(fs.existsSync(appPath)).toBe(false); expect(fs.existsSync(appPath)).toBe(false);
expect(po.getAppListItem({ appName })).not.toBeVisible(); expect(po.getAppListItem({ appName })).not.toBeVisible();
}); });

View File

@@ -0,0 +1 @@
avoid AI_RULES auto-prompt

View File

@@ -0,0 +1 @@
a

View File

@@ -0,0 +1 @@
b

View File

@@ -0,0 +1 @@
dir/c.txt

View File

@@ -0,0 +1 @@
this file should be deleted

View File

@@ -0,0 +1 @@
before-edit

View File

@@ -0,0 +1,10 @@
Deleting a file
<dyad-delete path="to-be-deleted.txt"></dyad-delete>
<dyad-write path="new-file.js" description="new file">
new-file
end of new-file
</dyad-write>
<dyad-write path="to-be-edited.txt" description="editing file">
after-edit
</dyad-write>

View File

@@ -0,0 +1,2 @@
Moving a file
<dyad-rename from="dir/c.txt" to="new-dir/d.txt"></dyad-rename>

View File

@@ -15,7 +15,7 @@ export const Timeout = {
MEDIUM: os.platform() === "win32" ? 30_000 : 15_000, MEDIUM: os.platform() === "win32" ? 30_000 : 15_000,
}; };
class PageObject { export class PageObject {
private userDataDir: string; private userDataDir: string;
constructor( constructor(
@@ -26,11 +26,17 @@ class PageObject {
this.userDataDir = userDataDir; this.userDataDir = userDataDir;
} }
async setUp({ autoApprove = false }: { autoApprove?: boolean } = {}) { async setUp({
autoApprove = false,
nativeGit = false,
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
await this.goToSettingsTab(); await this.goToSettingsTab();
if (autoApprove) { if (autoApprove) {
await this.toggleAutoApprove(); await this.toggleAutoApprove();
} }
if (nativeGit) {
await this.toggleNativeGit();
}
await this.setUpTestProvider(); await this.setUpTestProvider();
await this.setUpTestModel(); await this.setUpTestModel();
@@ -67,6 +73,37 @@ class PageObject {
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape'); // await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
} }
async snapshotAppFiles({ name }: { name?: string } = {}) {
const appPath = await this.getCurrentAppPath();
if (!appPath || !fs.existsSync(appPath)) {
throw new Error(`App path does not exist: ${appPath}`);
}
await expect(() => {
const filesData = generateAppFilesSnapshotData(appPath, appPath, [
".git",
"node_modules",
// Avoid snapshotting lock files because they are getting generated
// automatically and cause noise, and not super important anyways.
"package-lock.json",
"pnpm-lock.yaml",
]);
// Sort by relative path to ensure deterministic output
filesData.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
const snapshotContent = filesData
.map((file) => `=== ${file.relativePath} ===\n${file.content}`)
.join("\n\n");
if (name) {
expect(snapshotContent).toMatchSnapshot(name);
} else {
expect(snapshotContent).toMatchSnapshot();
}
}).toPass();
}
async snapshotMessages({ async snapshotMessages({
replaceDumpPath = false, replaceDumpPath = false,
}: { replaceDumpPath?: boolean } = {}) { }: { replaceDumpPath?: boolean } = {}) {
@@ -134,9 +171,10 @@ class PageObject {
return this.page.getByTestId("preview-iframe-element"); return this.page.getByTestId("preview-iframe-element");
} }
async snapshotPreview() { async snapshotPreview({ name }: { name?: string } = {}) {
const iframe = this.getPreviewIframeElement(); const iframe = this.getPreviewIframeElement();
await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({ await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({
name,
timeout: Timeout.LONG, timeout: Timeout.LONG,
}); });
} }
@@ -299,7 +337,21 @@ class PageObject {
return this.page.getByTestId(`app-list-item-${appName}`); return this.page.getByTestId(`app-list-item-${appName}`);
} }
async isCurrentAppNameNone() {
await expect(async () => {
await expect(this.getTitleBarAppNameButton()).toContainText(
"no app selected",
);
}).toPass();
}
async getCurrentAppName() { async getCurrentAppName() {
// Make sure to wait for the app to be set to avoid a race condition.
await expect(async () => {
await expect(this.getTitleBarAppNameButton()).not.toContainText(
"no app selected",
);
}).toPass();
return (await this.getTitleBarAppNameButton().textContent())?.replace( return (await this.getTitleBarAppNameButton().textContent())?.replace(
"App: ", "App: ",
"", "",
@@ -338,6 +390,10 @@ class PageObject {
await this.page.getByRole("switch", { name: "Auto-approve" }).click(); await this.page.getByRole("switch", { name: "Auto-approve" }).click();
} }
async toggleNativeGit() {
await this.page.getByRole("switch", { name: "Enable Native Git" }).click();
}
async snapshotSettings() { async snapshotSettings() {
const settings = path.join(this.userDataDir, "user-settings.json"); const settings = path.join(this.userDataDir, "user-settings.json");
const settingsContent = fs.readFileSync(settings, "utf-8"); const settingsContent = fs.readFileSync(settings, "utf-8");
@@ -588,3 +644,48 @@ function prettifyDump(
}) })
.join("\n\n"); .join("\n\n");
} }
interface FileSnapshotData {
relativePath: string;
content: string;
}
function generateAppFilesSnapshotData(
currentPath: string,
basePath: string,
ignorePatterns: string[],
): FileSnapshotData[] {
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
let files: FileSnapshotData[] = [];
// Sort entries for deterministic order
entries.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of entries) {
const entryPath = path.join(currentPath, entry.name);
if (ignorePatterns.includes(entry.name)) {
continue;
}
if (entry.isDirectory()) {
files = files.concat(
generateAppFilesSnapshotData(entryPath, basePath, ignorePatterns),
);
} else if (entry.isFile()) {
const relativePath = path.relative(basePath, entryPath);
try {
const content = fs.readFileSync(entryPath, "utf-8");
files.push({ relativePath, content });
} catch (error) {
// Could be a binary file or permission issue, log and add a placeholder
const e = error as Error;
console.warn(`Could not read file ${entryPath}: ${e.message}`);
files.push({
relativePath,
content: `[Error reading file: ${e.message}]`,
});
}
}
}
return files;
}

View File

@@ -0,0 +1,7 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/

View File

@@ -0,0 +1,4 @@
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- text: Testing:write-index!

View File

@@ -0,0 +1,18 @@
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== dir/c.txt ===
dir/c.txt
=== to-be-deleted.txt ===
this file should be deleted
=== to-be-edited.txt ===
before-edit

View File

@@ -0,0 +1,19 @@
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== dir/c.txt ===
dir/c.txt
=== new-file.js ===
new-file
end of new-file
=== to-be-edited.txt ===
after-edit

View File

@@ -0,0 +1,19 @@
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== new-dir/d.txt ===
dir/c.txt
=== new-file.js ===
new-file
end of new-file
=== to-be-edited.txt ===
after-edit

View File

@@ -1,22 +1,30 @@
import { testSkipIfWindows } from "./helpers/test_helper"; import { PageObject, testSkipIfWindows } from "./helpers/test_helper";
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
testSkipIfWindows("switch versions", async ({ po }) => { const runSwitchVersionTest = async (po: PageObject, nativeGit: boolean) => {
await po.setUp({ autoApprove: true }); await po.setUp({ autoApprove: true, nativeGit });
await po.sendPrompt("tc=write-index"); await po.sendPrompt("tc=write-index");
await po.snapshotPreview(); await po.snapshotPreview({ name: `v2` });
expect( expect(
await po.page.getByRole("button", { name: "Version" }).textContent(), await po.page.getByRole("button", { name: "Version" }).textContent(),
).toBe("Version 2"); ).toBe("Version 2");
await po.page.getByRole("button", { name: "Version" }).click(); await po.page.getByRole("button", { name: "Version" }).click();
await po.page.getByText("Init Dyad app Undo").click(); await po.page.getByText("Init Dyad app Undo").click();
await po.snapshotPreview(); await po.snapshotPreview({ name: `v1` });
await po.page.getByRole("button", { name: "Undo to latest version" }).click(); await po.page.getByRole("button", { name: "Undo to latest version" }).click();
// Should be same as the previous snapshot, but just to be sure. // Should be same as the previous snapshot, but just to be sure.
await po.snapshotPreview(); await po.snapshotPreview({ name: `v1` });
await expect(po.page.getByText("Version 3")).toBeVisible(); await expect(po.page.getByText("Version 3")).toBeVisible();
};
testSkipIfWindows("switch versions", async ({ po }) => {
await runSwitchVersionTest(po, false);
});
testSkipIfWindows("switch versions with native git", async ({ po }) => {
await runSwitchVersionTest(po, true);
}); });

View File

@@ -15,6 +15,6 @@ test("create next.js app", async ({ po }) => {
await po.clickRestart(); await po.clickRestart();
// This can be pretty slow because it's waiting for the app to build. // This can be pretty slow because it's waiting for the app to build.
await expect(po.getPreviewIframeElement()).toBeVisible({ timeout: 50_000 }); await expect(po.getPreviewIframeElement()).toBeVisible({ timeout: 100_000 });
await po.snapshotPreview(); await po.snapshotPreview();
}); });

View File

@@ -0,0 +1,51 @@
import { PageObject, testSkipIfWindows } from "./helpers/test_helper";
import * as eph from "electron-playwright-helpers";
import path from "node:path";
const runVersionIntegrityTest = async (po: PageObject, nativeGit: boolean) => {
await po.setUp({ autoApprove: true, nativeGit });
// Importing a simple app with a few files.
await po.page.getByRole("button", { name: "Import App" }).click();
await eph.stubDialog(po.electronApp, "showOpenDialog", {
filePaths: [
path.join(__dirname, "fixtures", "import-app", "version-integrity"),
],
});
await po.page.getByRole("button", { name: "Select Folder" }).click();
await po.page.getByRole("textbox", { name: "Enter new app name" }).click();
await po.page
.getByRole("textbox", { name: "Enter new app name" })
.fill("version-integrity-app");
await po.page.getByRole("button", { name: "Import" }).click();
// Initial snapshot
await po.snapshotAppFiles({ name: "v1" });
// Add a file and delete a file
await po.sendPrompt("tc=version-integrity-add-edit-delete");
await po.snapshotAppFiles({ name: "v2" });
// Move a file
await po.sendPrompt("tc=version-integrity-move-file");
await po.snapshotAppFiles({ name: "v3" });
// Open version pane
await po.page.getByRole("button", { name: "Version 3" }).click();
await po.page.getByText("Init Dyad app Undo").click();
await po.snapshotAppFiles({ name: "v1" });
await po.page.getByRole("button", { name: "Undo to latest version" }).click();
// Should be same as the previous snapshot, but just to be sure.
await po.snapshotAppFiles({ name: "v1" });
};
testSkipIfWindows("version integrity (git isomorphic)", async ({ po }) => {
await runVersionIntegrityTest(po, false);
});
testSkipIfWindows("version integrity (git native)", async ({ po }) => {
await runVersionIntegrityTest(po, true);
});

View File

@@ -23,7 +23,7 @@ import { getEnvVar } from "../utils/read_env";
import { readSettings } from "../../main/settings"; import { readSettings } from "../../main/settings";
import fixPath from "fix-path"; import fixPath from "fix-path";
import { getGitAuthor } from "../utils/git_author";
import killPort from "kill-port"; import killPort from "kill-port";
import util from "util"; import util from "util";
import log from "electron-log"; import log from "electron-log";
@@ -33,6 +33,7 @@ import { getLanguageModelProviders } from "../shared/language_model_helpers";
import { startProxy } from "../utils/start_proxy_server"; import { startProxy } from "../utils/start_proxy_server";
import { Worker } from "worker_threads"; import { Worker } from "worker_threads";
import { createFromTemplate } from "./createFromTemplate"; import { createFromTemplate } from "./createFromTemplate";
import { gitCommit } from "../utils/git_utils";
const logger = log.scope("app_handlers"); const logger = log.scope("app_handlers");
const handle = createLoggedHandler(logger); const handle = createLoggedHandler(logger);
@@ -207,11 +208,9 @@ export function registerAppHandlers() {
}); });
// Create initial commit // Create initial commit
const commitHash = await git.commit({ const commitHash = await gitCommit({
fs: fs, path: fullAppPath,
dir: fullAppPath,
message: "Init Dyad app", message: "Init Dyad app",
author: await getGitAuthor(),
}); });
// Update chat with initial commit hash // Update chat with initial commit hash
@@ -521,11 +520,9 @@ export function registerAppHandlers() {
filepath: filePath, filepath: filePath,
}); });
await git.commit({ await gitCommit({
fs, path: appPath,
dir: appPath,
message: `Updated ${filePath}`, message: `Updated ${filePath}`,
author: await getGitAuthor(),
}); });
} }

View File

@@ -9,9 +9,10 @@ import { db } from "@/db";
import { chats } from "@/db/schema"; import { chats } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import git from "isomorphic-git"; import git from "isomorphic-git";
import { getGitAuthor } from "../utils/git_author";
import { ImportAppParams, ImportAppResult } from "../ipc_types"; import { ImportAppParams, ImportAppResult } from "../ipc_types";
import { copyDirectoryRecursive } from "../utils/file_utils"; import { copyDirectoryRecursive } from "../utils/file_utils";
import { gitCommit } from "../utils/git_utils";
const logger = log.scope("import-handlers"); const logger = log.scope("import-handlers");
const handle = createLoggedHandler(logger); const handle = createLoggedHandler(logger);
@@ -114,11 +115,9 @@ export function registerImportHandlers() {
}); });
// Create initial commit // Create initial commit
await git.commit({ await gitCommit({
fs: fs, path: destPath,
dir: destPath,
message: "Init Dyad app", message: "Init Dyad app",
author: await getGitAuthor(),
}); });
} }

View File

@@ -5,12 +5,11 @@ import type { Version, BranchResult } from "../ipc_types";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { getDyadAppPath } from "../../paths/paths"; import { getDyadAppPath } from "../../paths/paths";
import git from "isomorphic-git"; import git, { type ReadCommitResult } from "isomorphic-git";
import { promises as fsPromises } from "node:fs";
import { withLock } from "../utils/lock_utils"; import { withLock } from "../utils/lock_utils";
import { getGitAuthor } from "../utils/git_author";
import log from "electron-log"; import log from "electron-log";
import { createLoggedHandler } from "./safe_handle"; import { createLoggedHandler } from "./safe_handle";
import { gitCheckout, gitCommit, gitStageToRevert } from "../utils/git_utils";
const logger = log.scope("version_handlers"); const logger = log.scope("version_handlers");
@@ -40,7 +39,7 @@ export function registerVersionHandlers() {
depth: 10_000, // Limit to last 10_000 commits for performance depth: 10_000, // Limit to last 10_000 commits for performance
}); });
return commits.map((commit) => ({ return commits.map((commit: ReadCommitResult) => ({
oid: commit.oid, oid: commit.oid,
message: commit.commit.message, message: commit.commit.message,
timestamp: commit.commit.author.timestamp, timestamp: commit.commit.author.timestamp,
@@ -102,65 +101,19 @@ export function registerVersionHandlers() {
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
await git.checkout({ await gitCheckout({
fs, path: appPath,
dir: appPath,
ref: "main", ref: "main",
force: true,
});
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
const matrix = await git.statusMatrix({
fs,
dir: appPath,
ref: previousVersionId,
}); });
// Process each file to revert to the state in previousVersionId await gitStageToRevert({
for (const [filepath, headStatus, workdirStatus] of matrix) { path: appPath,
const fullPath = path.join(appPath, filepath); targetOid: previousVersionId,
// If file exists in HEAD (previous version)
if (headStatus === 1) {
// If file doesn't exist or has changed in working directory, restore it from the target commit
if (workdirStatus !== 1) {
const { blob } = await git.readBlob({
fs,
dir: appPath,
oid: previousVersionId,
filepath,
});
await fsPromises.mkdir(path.dirname(fullPath), {
recursive: true,
});
await fsPromises.writeFile(fullPath, Buffer.from(blob));
}
}
// If file doesn't exist in HEAD but exists in working directory, delete it
else if (headStatus === 0 && workdirStatus !== 0) {
if (fs.existsSync(fullPath)) {
await fsPromises.unlink(fullPath);
await git.remove({
fs,
dir: appPath,
filepath: filepath,
});
}
}
}
// Stage all changes
await git.add({
fs,
dir: appPath,
filepath: ".",
}); });
// Create a revert commit await gitCommit({
await git.commit({ path: appPath,
fs,
dir: appPath,
message: `Reverted all changes back to version ${previousVersionId}`, message: `Reverted all changes back to version ${previousVersionId}`,
author: await getGitAuthor(),
}); });
// Find the chat and message associated with the commit hash // Find the chat and message associated with the commit hash
@@ -221,9 +174,8 @@ export function registerVersionHandlers() {
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
await git.checkout({ await gitCheckout({
fs, path: appPath,
dir: appPath,
ref: versionId, ref: versionId,
}); });
}); });

View File

@@ -6,7 +6,6 @@ import { getDyadAppPath } from "../../paths/paths";
import path from "node:path"; import path from "node:path";
import git from "isomorphic-git"; import git from "isomorphic-git";
import { getGitAuthor } from "../utils/git_author";
import log from "electron-log"; import log from "electron-log";
import { executeAddDependency } from "./executeAddDependency"; import { executeAddDependency } from "./executeAddDependency";
import { import {
@@ -16,6 +15,7 @@ import {
} from "../../supabase_admin/supabase_management_client"; } from "../../supabase_admin/supabase_management_client";
import { isServerFunction } from "../../supabase_admin/supabase_utils"; import { isServerFunction } from "../../supabase_admin/supabase_utils";
import { SqlQuery } from "../../lib/schemas"; import { SqlQuery } from "../../lib/schemas";
import { gitCommit } from "../utils/git_utils";
const readFile = fs.promises.readFile; const readFile = fs.promises.readFile;
const logger = log.scope("response_processor"); const logger = log.scope("response_processor");
@@ -460,11 +460,9 @@ export async function processFullResponseActions(
? `[dyad] ${chatSummary} - ${changes.join(", ")}` ? `[dyad] ${chatSummary} - ${changes.join(", ")}`
: `[dyad] ${changes.join(", ")}`; : `[dyad] ${changes.join(", ")}`;
// Use chat summary, if provided, or default for commit message // Use chat summary, if provided, or default for commit message
let commitHash = await git.commit({ let commitHash = await gitCommit({
fs, path: appPath,
dir: appPath,
message, message,
author: await getGitAuthor(),
}); });
logger.log(`Successfully committed changes: ${changes.join(", ")}`); logger.log(`Successfully committed changes: ${changes.join(", ")}`);
@@ -482,11 +480,9 @@ export async function processFullResponseActions(
filepath: ".", filepath: ".",
}); });
try { try {
commitHash = await git.commit({ commitHash = await gitCommit({
fs, path: appPath,
dir: appPath,
message: message + " + extra files edited outside of Dyad", message: message + " + extra files edited outside of Dyad",
author: await getGitAuthor(),
amend: true, amend: true,
}); });
logger.log( logger.log(

140
src/ipc/utils/git_utils.ts Normal file
View File

@@ -0,0 +1,140 @@
import { getGitAuthor } from "./git_author";
import git from "isomorphic-git";
import fs from "node:fs";
import { promises as fsPromises } from "node:fs";
import pathModule from "node:path";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { readSettings } from "../../main/settings";
const execAsync = promisify(exec);
export async function gitCommit({
path,
message,
amend,
}: {
path: string;
message: string;
amend?: boolean;
}): Promise<string> {
const settings = readSettings();
if (settings.enableNativeGit) {
let command = `git -C "${path}" commit -m "${message.replace(/"/g, '\\"')}"`;
if (amend) {
command += " --amend";
}
await execAsync(command);
const { stdout } = await execAsync(`git -C "${path}" rev-parse HEAD`);
return stdout.trim();
} else {
return git.commit({
fs: fs,
dir: path,
message,
author: await getGitAuthor(),
amend: amend,
});
}
}
export async function gitCheckout({
path,
ref,
}: {
path: string;
ref: string;
}): Promise<void> {
const settings = readSettings();
if (settings.enableNativeGit) {
await execAsync(`git -C "${path}" checkout "${ref.replace(/"/g, '\\"')}"`);
return;
} else {
return git.checkout({ fs, dir: path, ref });
}
}
export async function gitStageToRevert({
path,
targetOid,
}: {
path: string;
targetOid: string;
}): Promise<void> {
const settings = readSettings();
if (settings.enableNativeGit) {
// Get the current HEAD commit hash
const { stdout: currentHead } = await execAsync(
`git -C "${path}" rev-parse HEAD`,
);
const currentCommit = currentHead.trim();
// If we're already at the target commit, nothing to do
if (currentCommit === targetOid) {
return;
}
// Safety: refuse to run if the work-tree isnt clean.
const { stdout: wtStatus } = await execAsync(
`git -C "${path}" status --porcelain`,
);
if (wtStatus.trim() !== "") {
throw new Error("Cannot revert: working tree has uncommitted changes.");
}
// Reset the working directory and index to match the target commit state
// This effectively undoes all changes since the target commit
await execAsync(`git -C "${path}" reset --hard "${targetOid}"`);
// Reset back to the original HEAD but keep the working directory as it is
// This stages all the changes needed to revert to the target state
await execAsync(`git -C "${path}" reset --soft "${currentCommit}"`);
} else {
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
const matrix = await git.statusMatrix({
fs,
dir: path,
ref: targetOid,
});
// Process each file to revert to the state in previousVersionId
for (const [filepath, headStatus, workdirStatus] of matrix) {
const fullPath = pathModule.join(path, filepath);
// If file exists in HEAD (previous version)
if (headStatus === 1) {
// If file doesn't exist or has changed in working directory, restore it from the target commit
if (workdirStatus !== 1) {
const { blob } = await git.readBlob({
fs,
dir: path,
oid: targetOid,
filepath,
});
await fsPromises.mkdir(pathModule.dirname(fullPath), {
recursive: true,
});
await fsPromises.writeFile(fullPath, Buffer.from(blob));
}
}
// If file doesn't exist in HEAD but exists in working directory, delete it
else if (headStatus === 0 && workdirStatus !== 0) {
if (fs.existsSync(fullPath)) {
await fsPromises.unlink(fullPath);
await git.remove({
fs,
dir: path,
filepath: filepath,
});
}
}
}
// Stage all changes
await git.add({
fs,
dir: path,
filepath: ".",
});
}
}

View File

@@ -122,6 +122,8 @@ export const UserSettingsSchema = z.object({
enableProSmartFilesContextMode: z.boolean().optional(), enableProSmartFilesContextMode: z.boolean().optional(),
selectedTemplateId: z.string().optional(), selectedTemplateId: z.string().optional(),
enableNativeGit: z.boolean().optional(),
//////////////////////////////// ////////////////////////////////
// E2E TESTING ONLY. // E2E TESTING ONLY.
//////////////////////////////// ////////////////////////////////

View File

@@ -15,6 +15,7 @@ import { useRouter } from "@tanstack/react-router";
import { GitHubIntegration } from "@/components/GitHubIntegration"; import { GitHubIntegration } from "@/components/GitHubIntegration";
import { SupabaseIntegration } from "@/components/SupabaseIntegration"; import { SupabaseIntegration } from "@/components/SupabaseIntegration";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
export default function SettingsPage() { export default function SettingsPage() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
@@ -108,6 +109,35 @@ export default function SettingsPage() {
</div> </div>
</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"> <div className="mt-4">
<MaxChatTurnsSelector /> <MaxChatTurnsSelector />
</div> </div>