diff --git a/src/components/ProModeSelector.tsx b/src/components/ProModeSelector.tsx index 047bb62..0ca2248 100644 --- a/src/components/ProModeSelector.tsx +++ b/src/components/ProModeSelector.tsx @@ -27,6 +27,12 @@ export function ProModeSelector() { }); }; + const toggleSmartContext = () => { + updateSettings({ + enableProSmartFilesContextMode: !settings?.enableProSmartFilesContextMode, + }); + }; + if (!settings?.enableDyadPro) { return null; } @@ -105,6 +111,30 @@ export function ProModeSelector() { onCheckedChange={toggleLazyEdits} /> +
+
+ +
+ + + + + + Improve efficiency and save credits working on large + codebases. + + +

+ Automatically detects the most relevant files for your chat +

+
+
+ +
diff --git a/src/components/chat/DyadCodebaseContext.tsx b/src/components/chat/DyadCodebaseContext.tsx new file mode 100644 index 0000000..eaa6a3d --- /dev/null +++ b/src/components/chat/DyadCodebaseContext.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect } from "react"; +import { ChevronUp, ChevronDown, Code2, FileText } from "lucide-react"; +import { CustomTagState } from "./stateTypes"; + +interface DyadCodebaseContextProps { + children: React.ReactNode; + node?: { + properties?: { + files?: string; + state?: CustomTagState; + }; + }; +} + +export const DyadCodebaseContext: React.FC = ({ + node, +}) => { + const state = node?.properties?.state as CustomTagState; + const inProgress = state === "pending"; + const [isExpanded, setIsExpanded] = useState(inProgress); + const files = node?.properties?.files?.split(",") || []; + + // Collapse when transitioning from in-progress to not-in-progress + useEffect(() => { + if (!inProgress && isExpanded) { + setIsExpanded(false); + } + }, [inProgress]); + + return ( +
setIsExpanded(!isExpanded)} + role="button" + aria-expanded={isExpanded} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsExpanded(!isExpanded); + } + }} + > + {/* Top-left label badge */} +
+ + Codebase Context +
+ + {/* File count when collapsed */} + {files.length > 0 && ( +
+ + Using {files.length} file{files.length !== 1 ? "s" : ""} + +
+ )} + + {/* Indicator icon */} +
+ {isExpanded ? : } +
+ + {/* Main content with smooth transition */} +
+ {/* File list when expanded */} + {files.length > 0 && ( +
+
+ {files.map((file, index) => { + const filePath = file.trim(); + const fileName = filePath.split("/").pop() || filePath; + const pathPart = + filePath.substring(0, filePath.length - fileName.length) || + ""; + + return ( +
+
+ +
+ {fileName} +
+
+ {pathPart && ( +
+ {pathPart} +
+ )} +
+ ); + })} +
+
+ )} +
+
+ ); +}; diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx index a1e08d8..ea22f3d 100644 --- a/src/components/chat/DyadMarkdownParser.tsx +++ b/src/components/chat/DyadMarkdownParser.tsx @@ -8,6 +8,7 @@ import { DyadAddDependency } from "./DyadAddDependency"; import { DyadExecuteSql } from "./DyadExecuteSql"; import { DyadAddIntegration } from "./DyadAddIntegration"; import { DyadEdit } from "./DyadEdit"; +import { DyadCodebaseContext } from "./DyadCodebaseContext"; import { CodeHighlight } from "./CodeHighlight"; import { useAtomValue } from "jotai"; import { isStreamingAtom } from "@/atoms/chatAtoms"; @@ -117,6 +118,7 @@ function preprocessUnclosedTags(content: string): { "dyad-output", "dyad-chat-summary", "dyad-edit", + "dyad-codebase-context", ]; let processedContent = content; @@ -180,6 +182,7 @@ function parseCustomTags(content: string): ContentPiece[] { "dyad-output", "dyad-chat-summary", "dyad-edit", + "dyad-codebase-context", ]; const tagPattern = new RegExp( @@ -362,6 +365,20 @@ function renderCustomTag( ); + case "dyad-codebase-context": + return ( + + {content} + + ); + case "dyad-output": return ( ({ @@ -328,15 +325,22 @@ This conversation includes one or more image attachments. When the user uploads `; } + const codebasePrefix = isEngineEnabled + ? // No codebase prefix if engine is set, we will take of it there. + [] + : ([ + { + role: "user", + content: "This is my codebase. " + codebaseInfo, + }, + { + role: "assistant", + content: "OK, got it. I'm ready to help", + }, + ] as const); + let chatMessages: CoreMessage[] = [ - { - role: "user", - content: "This is my codebase. " + codebaseInfo, - }, - { - role: "assistant", - content: "OK, got it. I'm ready to help", - }, + ...codebasePrefix, ...limitedMessageHistory.map((msg) => ({ role: msg.role as "user" | "assistant" | "system", content: msg.content, diff --git a/src/ipc/utils/get_model_client.ts b/src/ipc/utils/get_model_client.ts index bc3249e..a9c4bb8 100644 --- a/src/ipc/utils/get_model_client.ts +++ b/src/ipc/utils/get_model_client.ts @@ -50,6 +50,7 @@ export async function getModelClient( ): Promise<{ modelClient: ModelClient; backupModelClients: ModelClient[]; + isEngineEnabled?: boolean; }> { const allProviders = await getLanguageModelProviders(); @@ -103,9 +104,12 @@ export async function getModelClient( // so we do a nullish and not a truthy check here. if (providerConfig.gatewayPrefix != null || dyadLocalEngine) { const languageModel = await findLanguageModel(model); + const engineProMode = + settings.enableProSmartFilesContextMode || + settings.enableProLazyEditsMode; // Currently engine is only used for turbo edits. const isEngineEnabled = Boolean( - settings.enableProLazyEditsMode && + engineProMode && languageModel?.type === "cloud" && languageModel?.supportsTurboEdits, ); @@ -113,6 +117,10 @@ export async function getModelClient( ? createDyadEngine({ apiKey: dyadApiKey, baseURL: dyadLocalEngine ?? "https://engine.dyad.sh/v1", + dyadOptions: { + enableLazyEdits: settings.enableProLazyEditsMode, + enableSmartFilesContext: settings.enableProSmartFilesContextMode, + }, }) : createOpenAICompatible({ name: "dyad-gateway", @@ -126,7 +134,7 @@ export async function getModelClient( const autoModelClient = { model: provider( `${providerConfig.gatewayPrefix || ""}${modelName}`, - settings.enableProLazyEditsMode + engineProMode ? { files, } @@ -161,6 +169,7 @@ export async function getModelClient( providerConfig, ).modelClient, backupModelClients: [autoModelClient], + isEngineEnabled, }; } else { return { diff --git a/src/ipc/utils/llm_engine_provider.ts b/src/ipc/utils/llm_engine_provider.ts index c325066..a445837 100644 --- a/src/ipc/utils/llm_engine_provider.ts +++ b/src/ipc/utils/llm_engine_provider.ts @@ -41,6 +41,11 @@ Custom fetch implementation. You can use it as a middleware to intercept request or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; + + dyadOptions: { + enableLazyEdits?: boolean; + enableSmartFilesContext?: boolean; + }; } export interface DyadEngineProvider { @@ -62,7 +67,7 @@ Creates a chat model for text generation. } export function createDyadEngine( - options: ExampleProviderSettings = {}, + options: ExampleProviderSettings, ): DyadEngineProvider { const baseURL = withoutTrailingSlash( options.baseURL ?? "https://api.example.com/v1", @@ -124,7 +129,9 @@ export function createDyadEngine( if (files?.length) { parsedBody.dyad_options = { files, - enable_lazy_edits: true, + enable_lazy_edits: options.dyadOptions.enableLazyEdits, + enable_smart_files_context: + options.dyadOptions.enableSmartFilesContext, }; } diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index f9d687e..56a2d62 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -120,6 +120,7 @@ export const UserSettingsSchema = z.object({ maxChatTurnsInContext: z.number().optional(), enableProSaverMode: z.boolean().optional(), enableProLazyEditsMode: z.boolean().optional(), + enableProSmartFilesContextMode: z.boolean().optional(), // DEPRECATED. runtimeMode: RuntimeModeSchema.optional(), }); diff --git a/src/utils/codebase.ts b/src/utils/codebase.ts index 2445fc6..84d4644 100644 --- a/src/utils/codebase.ts +++ b/src/utils/codebase.ts @@ -223,6 +223,21 @@ async function collectFiles(dir: string, baseDir: string): Promise { return files; } +// Skip large configuration files or generated code (just include the path) +function isOmittedFile(relativePath: string): boolean { + return ( + relativePath.includes(path.join("src", "components", "ui")) || + relativePath.includes("eslint.config") || + relativePath.includes("tsconfig.json") || + relativePath.includes("package-lock.json") || + // These should already be excluded based on file type, but + // just in case, we'll redact the contents here. + relativePath.includes(".env") + ); +} + +const OMITTED_FILE_CONTENT = "// Contents omitted for brevity"; + /** * Format a file for inclusion in the codebase extract */ @@ -230,18 +245,9 @@ async function formatFile(filePath: string, baseDir: string): Promise { try { const relativePath = path.relative(baseDir, filePath); - // Skip large configuration files or generated code (just include the path) - if ( - relativePath.includes(path.join("src", "components", "ui")) || - relativePath.includes("eslint.config") || - relativePath.includes("tsconfig.json") || - relativePath.includes("package-lock.json") || - // These should already be excluded based on file type, but - // just in case, we'll redact the contents here. - relativePath.includes(".env") - ) { + if (isOmittedFile(relativePath)) { return ` -// Contents omitted for brevity +${OMITTED_FILE_CONTENT} `; @@ -305,11 +311,13 @@ export async function extractCodebase(appPath: string): Promise<{ // Get raw content for the files array const relativePath = path.relative(appPath, file); - const rawContent = await readFileWithCache(file); - if (rawContent !== null) { + const fileContent = isOmittedFile(relativePath) + ? OMITTED_FILE_CONTENT + : await readFileWithCache(file); + if (fileContent !== null) { filesArray.push({ path: relativePath, - content: rawContent, + content: fileContent, }); }