diff --git a/package.json b/package.json
index 0eb3ca3..ed3f6fc 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"scripts": {
"clean": "rm -rf out && rm -rf scaffold/node_modules",
"start": "electron-forge start",
+ "dev:engine": "DYAD_LOCAL_ENGINE=http://localhost:8080/v1 npm start",
"package": "npm run clean && electron-forge package",
"make": "npm run clean && electron-forge make",
"publish": "npm run clean && electron-forge publish",
diff --git a/src/components/ProModeSelector.tsx b/src/components/ProModeSelector.tsx
index c8dca95..047bb62 100644
--- a/src/components/ProModeSelector.tsx
+++ b/src/components/ProModeSelector.tsx
@@ -20,6 +20,13 @@ export function ProModeSelector() {
const toggleSaverMode = () => {
updateSettings({ enableProSaverMode: !settings?.enableProSaverMode });
};
+
+ const toggleLazyEdits = () => {
+ updateSettings({
+ enableProLazyEditsMode: !settings?.enableProLazyEditsMode,
+ });
+ };
+
if (!settings?.enableDyadPro) {
return null;
}
@@ -75,6 +82,29 @@ export function ProModeSelector() {
onCheckedChange={toggleSaverMode}
/>
+
+
+
+
+
+
+
+
+
+ Edits files faster.
+
+
+
+ Makes editing files faster and cheaper.
+
+
+
+
+
diff --git a/src/components/chat/DyadEdit.tsx b/src/components/chat/DyadEdit.tsx
new file mode 100644
index 0000000..4eb9c5a
--- /dev/null
+++ b/src/components/chat/DyadEdit.tsx
@@ -0,0 +1,110 @@
+import type React from "react";
+import type { ReactNode } from "react";
+import { useState } from "react";
+import {
+ ChevronsDownUp,
+ ChevronsUpDown,
+ Loader,
+ CircleX,
+ Rabbit,
+} from "lucide-react";
+import { CodeHighlight } from "./CodeHighlight";
+import { CustomTagState } from "./stateTypes";
+
+interface DyadEditProps {
+ children?: ReactNode;
+ node?: any;
+ path?: string;
+ description?: string;
+}
+
+export const DyadEdit: React.FC = ({
+ children,
+ node,
+ path: pathProp,
+ description: descriptionProp,
+}) => {
+ const [isContentVisible, setIsContentVisible] = useState(false);
+
+ // Use props directly if provided, otherwise extract from node
+ const path = pathProp || node?.properties?.path || "";
+ const description = descriptionProp || node?.properties?.description || "";
+ const state = node?.properties?.state as CustomTagState;
+ const inProgress = state === "pending";
+ const aborted = state === "aborted";
+
+ // Extract filename from path
+ const fileName = path ? path.split("/").pop() : "";
+
+ return (
+ setIsContentVisible(!isContentVisible)}
+ >
+
+
+
+
+
+ Turbo Edit
+
+
+ {fileName && (
+
+ {fileName}
+
+ )}
+ {inProgress && (
+
+
+ Editing...
+
+ )}
+ {aborted && (
+
+
+ Did not finish
+
+ )}
+
+
+ {isContentVisible ? (
+
+ ) : (
+
+ )}
+
+
+ {path && (
+
+ {path}
+
+ )}
+ {description && (
+
+ Summary:
+ {description}
+
+ )}
+ {isContentVisible && (
+
+
+ {children}
+
+
+ )}
+
+ );
+};
diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx
index 1e0c9ea..a1e08d8 100644
--- a/src/components/chat/DyadMarkdownParser.tsx
+++ b/src/components/chat/DyadMarkdownParser.tsx
@@ -7,6 +7,7 @@ import { DyadDelete } from "./DyadDelete";
import { DyadAddDependency } from "./DyadAddDependency";
import { DyadExecuteSql } from "./DyadExecuteSql";
import { DyadAddIntegration } from "./DyadAddIntegration";
+import { DyadEdit } from "./DyadEdit";
import { CodeHighlight } from "./CodeHighlight";
import { useAtomValue } from "jotai";
import { isStreamingAtom } from "@/atoms/chatAtoms";
@@ -115,6 +116,7 @@ function preprocessUnclosedTags(content: string): {
"dyad-add-integration",
"dyad-output",
"dyad-chat-summary",
+ "dyad-edit",
];
let processedContent = content;
@@ -177,6 +179,7 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-add-integration",
"dyad-output",
"dyad-chat-summary",
+ "dyad-edit",
];
const tagPattern = new RegExp(
@@ -344,6 +347,21 @@ function renderCustomTag(
);
+ case "dyad-edit":
+ return (
+
+ {content}
+
+ );
+
case "dyad-output":
return (
({
diff --git a/src/ipc/handlers/debug_handlers.ts b/src/ipc/handlers/debug_handlers.ts
index 26d37cb..1fee9d1 100644
--- a/src/ipc/handlers/debug_handlers.ts
+++ b/src/ipc/handlers/debug_handlers.ts
@@ -140,7 +140,7 @@ export function registerDebugHandlers() {
// Extract codebase
const appPath = getDyadAppPath(app.path);
- const codebase = await extractCodebase(appPath);
+ const codebase = (await extractCodebase(appPath)).formattedOutput;
return {
debugInfo,
diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts
index 6d94495..3b54f7f 100644
--- a/src/ipc/handlers/proposal_handlers.ts
+++ b/src/ipc/handlers/proposal_handlers.ts
@@ -92,7 +92,8 @@ async function getCodebaseTokenCount(
// Calculate and cache the token count
logger.log(`Calculating codebase token count for chatId: ${chatId}`);
- const codebase = await extractCodebase(getDyadAppPath(appPath));
+ const codebase = (await extractCodebase(getDyadAppPath(appPath)))
+ .formattedOutput;
const tokenCount = estimateTokens(codebase);
// Store in cache
diff --git a/src/ipc/handlers/token_count_handlers.ts b/src/ipc/handlers/token_count_handlers.ts
index 198f4d0..dcfab8e 100644
--- a/src/ipc/handlers/token_count_handlers.ts
+++ b/src/ipc/handlers/token_count_handlers.ts
@@ -68,7 +68,7 @@ export function registerTokenCountHandlers() {
if (chat.app) {
const appPath = getDyadAppPath(chat.app.path);
- codebaseInfo = await extractCodebase(appPath);
+ codebaseInfo = (await extractCodebase(appPath)).formattedOutput;
codebaseTokens = estimateTokens(codebaseInfo);
logger.log(
`Extracted codebase information from ${appPath}, tokens: ${codebaseTokens}`,
diff --git a/src/ipc/utils/get_model_client.ts b/src/ipc/utils/get_model_client.ts
index b8aa146..4de54c9 100644
--- a/src/ipc/utils/get_model_client.ts
+++ b/src/ipc/utils/get_model_client.ts
@@ -11,6 +11,9 @@ import log from "electron-log";
import { getLanguageModelProviders } from "../shared/language_model_helpers";
import { LanguageModelProvider } from "../ipc_types";
import { llmErrorStore } from "@/main/llm_error_store";
+import { createDyadEngine } from "./llm_engine_provider";
+
+const dyadLocalEngine = process.env.DYAD_LOCAL_ENGINE;
const AUTO_MODELS = [
{
@@ -32,10 +35,16 @@ export interface ModelClient {
builtinProviderId?: string;
}
+interface File {
+ path: string;
+ content: string;
+}
+
const logger = log.scope("getModelClient");
export async function getModelClient(
model: LargeLanguageModel,
settings: UserSettings,
+ files?: File[],
): Promise<{
modelClient: ModelClient;
backupModelClients: ModelClient[];
@@ -65,8 +74,9 @@ export async function getModelClient(
{
provider: autoModel.provider,
name: autoModel.name,
- } as LargeLanguageModel,
+ },
settings,
+ files,
);
}
}
@@ -85,17 +95,33 @@ export async function getModelClient(
// Handle Dyad Pro override
if (dyadApiKey && settings.enableDyadPro) {
- // Check if the selected provider supports Dyad Pro (has a gateway prefix)
- if (providerConfig.gatewayPrefix) {
- const provider = createOpenAI({
- apiKey: dyadApiKey,
- baseURL: "https://llm-gateway.dyad.sh/v1",
- });
- logger.info("Using Dyad Pro API key via Gateway");
+ // Check if the selected provider supports Dyad Pro (has a gateway prefix) OR
+ // we're using local engine.
+ if (providerConfig.gatewayPrefix || dyadLocalEngine) {
+ const provider = settings.enableProLazyEditsMode
+ ? createDyadEngine({
+ apiKey: dyadApiKey,
+ baseURL: dyadLocalEngine ?? "https://engine.dyad.sh/v1",
+ })
+ : createOpenAI({
+ apiKey: dyadApiKey,
+ baseURL: "https://llm-gateway.dyad.sh/v1",
+ });
+
+ logger.info(
+ `Using Dyad Pro API key. engine_enabled=${settings.enableProLazyEditsMode}`,
+ );
// Do not use free variant (for openrouter).
const modelName = model.name.split(":free")[0];
const autoModelClient = {
- model: provider(`${providerConfig.gatewayPrefix}${modelName}`),
+ model: provider(
+ `${providerConfig.gatewayPrefix || ""}${modelName}`,
+ settings.enableProLazyEditsMode
+ ? {
+ files,
+ }
+ : undefined,
+ ),
builtinProviderId: "auto",
};
const googleSettings = settings.providerSettings?.google;
@@ -235,7 +261,7 @@ function getRegularModelClient(
const provider = createOpenAICompatible({
name: providerConfig.id,
baseURL: providerConfig.apiBaseUrl,
- apiKey: apiKey,
+ apiKey,
});
return {
modelClient: {
diff --git a/src/ipc/utils/llm_engine_provider.ts b/src/ipc/utils/llm_engine_provider.ts
new file mode 100644
index 0000000..c325066
--- /dev/null
+++ b/src/ipc/utils/llm_engine_provider.ts
@@ -0,0 +1,159 @@
+import {
+ LanguageModelV1,
+ LanguageModelV1ObjectGenerationMode,
+} from "@ai-sdk/provider";
+import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible";
+import {
+ FetchFunction,
+ loadApiKey,
+ withoutTrailingSlash,
+} from "@ai-sdk/provider-utils";
+
+import { OpenAICompatibleChatSettings } from "@ai-sdk/openai-compatible";
+import log from "electron-log";
+
+const logger = log.scope("llm_engine_provider");
+
+export type ExampleChatModelId = string & {};
+
+export interface ExampleChatSettings extends OpenAICompatibleChatSettings {
+ files?: { path: string; content: string }[];
+}
+export interface ExampleProviderSettings {
+ /**
+Example API key.
+*/
+ apiKey?: string;
+ /**
+Base URL for the API calls.
+*/
+ baseURL?: string;
+ /**
+Custom headers to include in the requests.
+*/
+ headers?: Record;
+ /**
+Optional custom url query parameters to include in request urls.
+*/
+ queryParams?: Record;
+ /**
+Custom fetch implementation. You can use it as a middleware to intercept requests,
+or to provide a custom fetch implementation for e.g. testing.
+*/
+ fetch?: FetchFunction;
+}
+
+export interface DyadEngineProvider {
+ /**
+Creates a model for text generation.
+*/
+ (
+ modelId: ExampleChatModelId,
+ settings?: ExampleChatSettings,
+ ): LanguageModelV1;
+
+ /**
+Creates a chat model for text generation.
+*/
+ chatModel(
+ modelId: ExampleChatModelId,
+ settings?: ExampleChatSettings,
+ ): LanguageModelV1;
+}
+
+export function createDyadEngine(
+ options: ExampleProviderSettings = {},
+): DyadEngineProvider {
+ const baseURL = withoutTrailingSlash(
+ options.baseURL ?? "https://api.example.com/v1",
+ );
+ const getHeaders = () => ({
+ Authorization: `Bearer ${loadApiKey({
+ apiKey: options.apiKey,
+ environmentVariableName: "DYAD_PRO_API_KEY",
+ description: "Example API key",
+ })}`,
+ ...options.headers,
+ });
+
+ interface CommonModelConfig {
+ provider: string;
+ url: ({ path }: { path: string }) => string;
+ headers: () => Record;
+ fetch?: FetchFunction;
+ }
+
+ const getCommonModelConfig = (modelType: string): CommonModelConfig => ({
+ provider: `example.${modelType}`,
+ url: ({ path }) => {
+ const url = new URL(`${baseURL}${path}`);
+ if (options.queryParams) {
+ url.search = new URLSearchParams(options.queryParams).toString();
+ }
+ return url.toString();
+ },
+ headers: getHeaders,
+ fetch: options.fetch,
+ });
+
+ const createChatModel = (
+ modelId: ExampleChatModelId,
+ settings: ExampleChatSettings = {},
+ ) => {
+ // Extract files from settings to process them appropriately
+ const { files, ...restSettings } = settings;
+
+ // Create configuration with file handling
+ const config = {
+ ...getCommonModelConfig("chat"),
+ defaultObjectGenerationMode:
+ "tool" as LanguageModelV1ObjectGenerationMode,
+ // Custom fetch implementation that adds files to the request
+ fetch: files?.length
+ ? (input: RequestInfo | URL, init?: RequestInit) => {
+ // Use default fetch if no init or body
+ if (!init || !init.body || typeof init.body !== "string") {
+ return (options.fetch || fetch)(input, init);
+ }
+
+ try {
+ // Parse the request body to manipulate it
+ const parsedBody = JSON.parse(init.body);
+
+ // Add files to the request if they exist
+ if (files?.length) {
+ parsedBody.dyad_options = {
+ files,
+ enable_lazy_edits: true,
+ };
+ }
+
+ // Return modified request with files included
+ const modifiedInit = {
+ ...init,
+ body: JSON.stringify(parsedBody),
+ };
+
+ // Use the provided fetch or default fetch
+ return (options.fetch || fetch)(input, modifiedInit);
+ } catch (e) {
+ logger.error("Error parsing request body", e);
+ // If parsing fails, use original request
+ return (options.fetch || fetch)(input, init);
+ }
+ }
+ : options.fetch,
+ };
+
+ return new OpenAICompatibleChatLanguageModel(modelId, restSettings, config);
+ };
+
+ const provider = (
+ modelId: ExampleChatModelId,
+ settings?: ExampleChatSettings,
+ ) => createChatModel(modelId, settings);
+
+ provider.chatModel = createChatModel;
+
+ return provider;
+}
diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts
index 85ecc99..f9d687e 100644
--- a/src/lib/schemas.ts
+++ b/src/lib/schemas.ts
@@ -119,6 +119,7 @@ export const UserSettingsSchema = z.object({
lastShownReleaseNotesVersion: z.string().optional(),
maxChatTurnsInContext: z.number().optional(),
enableProSaverMode: z.boolean().optional(),
+ enableProLazyEditsMode: z.boolean().optional(),
// DEPRECATED.
runtimeMode: RuntimeModeSchema.optional(),
});
diff --git a/src/utils/codebase.ts b/src/utils/codebase.ts
index e7bf1e0..2445fc6 100644
--- a/src/utils/codebase.ts
+++ b/src/utils/codebase.ts
@@ -275,13 +275,19 @@ ${content}
/**
* Extract and format codebase files as a string to be included in prompts
* @param appPath - Path to the codebase to extract
- * @returns A string containing formatted file contents
+ * @returns Object containing formatted output and individual files
*/
-export async function extractCodebase(appPath: string): Promise {
+export async function extractCodebase(appPath: string): Promise<{
+ formattedOutput: string;
+ files: { path: string; content: string }[];
+}> {
try {
await fsAsync.access(appPath);
} catch {
- return `# Error: Directory ${appPath} does not exist or is not accessible`;
+ return {
+ formattedOutput: `# Error: Directory ${appPath} does not exist or is not accessible`,
+ files: [],
+ };
}
const startTime = Date.now();
@@ -292,15 +298,33 @@ export async function extractCodebase(appPath: string): Promise {
// This is important for cache-ability.
const sortedFiles = await sortFilesByModificationTime(files);
- // Format files
- let output = "";
- const formatPromises = sortedFiles.map((file) => formatFile(file, appPath));
+ // Format files and collect individual file contents
+ const filesArray: { path: string; content: string }[] = [];
+ const formatPromises = sortedFiles.map(async (file) => {
+ const formattedContent = await formatFile(file, appPath);
+
+ // Get raw content for the files array
+ const relativePath = path.relative(appPath, file);
+ const rawContent = await readFileWithCache(file);
+ if (rawContent !== null) {
+ filesArray.push({
+ path: relativePath,
+ content: rawContent,
+ });
+ }
+
+ return formattedContent;
+ });
+
const formattedFiles = await Promise.all(formatPromises);
- output = formattedFiles.join("");
+ const formattedOutput = formattedFiles.join("");
const endTime = Date.now();
logger.log("extractCodebase: time taken", endTime - startTime);
- return output;
+ return {
+ formattedOutput,
+ files: filesArray,
+ };
}
/**