Smart files context (#184)
This commit is contained in:
@@ -27,6 +27,12 @@ export function ProModeSelector() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleSmartContext = () => {
|
||||||
|
updateSettings({
|
||||||
|
enableProSmartFilesContextMode: !settings?.enableProSmartFilesContextMode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!settings?.enableDyadPro) {
|
if (!settings?.enableDyadPro) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -105,6 +111,30 @@ export function ProModeSelector() {
|
|||||||
onCheckedChange={toggleLazyEdits}
|
onCheckedChange={toggleLazyEdits}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smart-context">Smart Context</Label>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className="max-w-72">
|
||||||
|
Improve efficiency and save credits working on large
|
||||||
|
codebases.
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-55">
|
||||||
|
Automatically detects the most relevant files for your chat
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="smart-context"
|
||||||
|
checked={Boolean(settings?.enableProSmartFilesContextMode)}
|
||||||
|
onCheckedChange={toggleSmartContext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
117
src/components/chat/DyadCodebaseContext.tsx
Normal file
117
src/components/chat/DyadCodebaseContext.tsx
Normal file
@@ -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<DyadCodebaseContextProps> = ({
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={`relative bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
|
||||||
|
inProgress ? "border-blue-500" : "border-border"
|
||||||
|
}`}
|
||||||
|
onClick={() => 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 */}
|
||||||
|
<div
|
||||||
|
className="absolute top-2 left-2 flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold text-blue-500 bg-white dark:bg-zinc-900"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<Code2 size={16} className="text-blue-500" />
|
||||||
|
<span>Codebase Context</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File count when collapsed */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="absolute top-2 left-40 flex items-center">
|
||||||
|
<span className="px-1.5 py-0.5 bg-gray-100 dark:bg-zinc-800 text-xs rounded text-gray-600 dark:text-gray-300">
|
||||||
|
Using {files.length} file{files.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicator icon */}
|
||||||
|
<div className="absolute top-2 right-2 p-1 text-gray-500">
|
||||||
|
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content with smooth transition */}
|
||||||
|
<div
|
||||||
|
className="pt-6 overflow-hidden transition-all duration-300 ease-in-out"
|
||||||
|
style={{
|
||||||
|
maxHeight: isExpanded ? "1000px" : "0px",
|
||||||
|
opacity: isExpanded ? 1 : 0,
|
||||||
|
marginBottom: isExpanded ? "0" : "-6px", // Compensate for padding
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* File list when expanded */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{files.map((file, index) => {
|
||||||
|
const filePath = file.trim();
|
||||||
|
const fileName = filePath.split("/").pop() || filePath;
|
||||||
|
const pathPart =
|
||||||
|
filePath.substring(0, filePath.length - fileName.length) ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 bg-gray-100 dark:bg-zinc-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<FileText
|
||||||
|
size={14}
|
||||||
|
className="text-gray-500 dark:text-gray-400 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{fileName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{pathPart && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 ml-5">
|
||||||
|
{pathPart}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { DyadAddDependency } from "./DyadAddDependency";
|
|||||||
import { DyadExecuteSql } from "./DyadExecuteSql";
|
import { DyadExecuteSql } from "./DyadExecuteSql";
|
||||||
import { DyadAddIntegration } from "./DyadAddIntegration";
|
import { DyadAddIntegration } from "./DyadAddIntegration";
|
||||||
import { DyadEdit } from "./DyadEdit";
|
import { DyadEdit } from "./DyadEdit";
|
||||||
|
import { DyadCodebaseContext } from "./DyadCodebaseContext";
|
||||||
import { CodeHighlight } from "./CodeHighlight";
|
import { CodeHighlight } from "./CodeHighlight";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
||||||
@@ -117,6 +118,7 @@ function preprocessUnclosedTags(content: string): {
|
|||||||
"dyad-output",
|
"dyad-output",
|
||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
|
"dyad-codebase-context",
|
||||||
];
|
];
|
||||||
|
|
||||||
let processedContent = content;
|
let processedContent = content;
|
||||||
@@ -180,6 +182,7 @@ function parseCustomTags(content: string): ContentPiece[] {
|
|||||||
"dyad-output",
|
"dyad-output",
|
||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
|
"dyad-codebase-context",
|
||||||
];
|
];
|
||||||
|
|
||||||
const tagPattern = new RegExp(
|
const tagPattern = new RegExp(
|
||||||
@@ -362,6 +365,20 @@ function renderCustomTag(
|
|||||||
</DyadEdit>
|
</DyadEdit>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "dyad-codebase-context":
|
||||||
|
return (
|
||||||
|
<DyadCodebaseContext
|
||||||
|
node={{
|
||||||
|
properties: {
|
||||||
|
files: attributes.files || "",
|
||||||
|
state: getState({ isStreaming, inProgress }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadCodebaseContext>
|
||||||
|
);
|
||||||
|
|
||||||
case "dyad-output":
|
case "dyad-output":
|
||||||
return (
|
return (
|
||||||
<DyadOutput
|
<DyadOutput
|
||||||
|
|||||||
@@ -236,11 +236,8 @@ export function registerChatStreamHandlers() {
|
|||||||
"estimated tokens",
|
"estimated tokens",
|
||||||
codebaseInfo.length / 4,
|
codebaseInfo.length / 4,
|
||||||
);
|
);
|
||||||
const { modelClient, backupModelClients } = await getModelClient(
|
const { modelClient, backupModelClients, isEngineEnabled } =
|
||||||
settings.selectedModel,
|
await getModelClient(settings.selectedModel, settings, files);
|
||||||
settings,
|
|
||||||
files,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prepare message history for the AI
|
// Prepare message history for the AI
|
||||||
const messageHistory = updatedChat.messages.map((message) => ({
|
const messageHistory = updatedChat.messages.map((message) => ({
|
||||||
@@ -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[] = [
|
let chatMessages: CoreMessage[] = [
|
||||||
{
|
...codebasePrefix,
|
||||||
role: "user",
|
|
||||||
content: "This is my codebase. " + codebaseInfo,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
content: "OK, got it. I'm ready to help",
|
|
||||||
},
|
|
||||||
...limitedMessageHistory.map((msg) => ({
|
...limitedMessageHistory.map((msg) => ({
|
||||||
role: msg.role as "user" | "assistant" | "system",
|
role: msg.role as "user" | "assistant" | "system",
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export async function getModelClient(
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
modelClient: ModelClient;
|
modelClient: ModelClient;
|
||||||
backupModelClients: ModelClient[];
|
backupModelClients: ModelClient[];
|
||||||
|
isEngineEnabled?: boolean;
|
||||||
}> {
|
}> {
|
||||||
const allProviders = await getLanguageModelProviders();
|
const allProviders = await getLanguageModelProviders();
|
||||||
|
|
||||||
@@ -103,9 +104,12 @@ export async function getModelClient(
|
|||||||
// so we do a nullish and not a truthy check here.
|
// so we do a nullish and not a truthy check here.
|
||||||
if (providerConfig.gatewayPrefix != null || dyadLocalEngine) {
|
if (providerConfig.gatewayPrefix != null || dyadLocalEngine) {
|
||||||
const languageModel = await findLanguageModel(model);
|
const languageModel = await findLanguageModel(model);
|
||||||
|
const engineProMode =
|
||||||
|
settings.enableProSmartFilesContextMode ||
|
||||||
|
settings.enableProLazyEditsMode;
|
||||||
// Currently engine is only used for turbo edits.
|
// Currently engine is only used for turbo edits.
|
||||||
const isEngineEnabled = Boolean(
|
const isEngineEnabled = Boolean(
|
||||||
settings.enableProLazyEditsMode &&
|
engineProMode &&
|
||||||
languageModel?.type === "cloud" &&
|
languageModel?.type === "cloud" &&
|
||||||
languageModel?.supportsTurboEdits,
|
languageModel?.supportsTurboEdits,
|
||||||
);
|
);
|
||||||
@@ -113,6 +117,10 @@ export async function getModelClient(
|
|||||||
? createDyadEngine({
|
? createDyadEngine({
|
||||||
apiKey: dyadApiKey,
|
apiKey: dyadApiKey,
|
||||||
baseURL: dyadLocalEngine ?? "https://engine.dyad.sh/v1",
|
baseURL: dyadLocalEngine ?? "https://engine.dyad.sh/v1",
|
||||||
|
dyadOptions: {
|
||||||
|
enableLazyEdits: settings.enableProLazyEditsMode,
|
||||||
|
enableSmartFilesContext: settings.enableProSmartFilesContextMode,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
: createOpenAICompatible({
|
: createOpenAICompatible({
|
||||||
name: "dyad-gateway",
|
name: "dyad-gateway",
|
||||||
@@ -126,7 +134,7 @@ export async function getModelClient(
|
|||||||
const autoModelClient = {
|
const autoModelClient = {
|
||||||
model: provider(
|
model: provider(
|
||||||
`${providerConfig.gatewayPrefix || ""}${modelName}`,
|
`${providerConfig.gatewayPrefix || ""}${modelName}`,
|
||||||
settings.enableProLazyEditsMode
|
engineProMode
|
||||||
? {
|
? {
|
||||||
files,
|
files,
|
||||||
}
|
}
|
||||||
@@ -161,6 +169,7 @@ export async function getModelClient(
|
|||||||
providerConfig,
|
providerConfig,
|
||||||
).modelClient,
|
).modelClient,
|
||||||
backupModelClients: [autoModelClient],
|
backupModelClients: [autoModelClient],
|
||||||
|
isEngineEnabled,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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.
|
or to provide a custom fetch implementation for e.g. testing.
|
||||||
*/
|
*/
|
||||||
fetch?: FetchFunction;
|
fetch?: FetchFunction;
|
||||||
|
|
||||||
|
dyadOptions: {
|
||||||
|
enableLazyEdits?: boolean;
|
||||||
|
enableSmartFilesContext?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DyadEngineProvider {
|
export interface DyadEngineProvider {
|
||||||
@@ -62,7 +67,7 @@ Creates a chat model for text generation.
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createDyadEngine(
|
export function createDyadEngine(
|
||||||
options: ExampleProviderSettings = {},
|
options: ExampleProviderSettings,
|
||||||
): DyadEngineProvider {
|
): DyadEngineProvider {
|
||||||
const baseURL = withoutTrailingSlash(
|
const baseURL = withoutTrailingSlash(
|
||||||
options.baseURL ?? "https://api.example.com/v1",
|
options.baseURL ?? "https://api.example.com/v1",
|
||||||
@@ -124,7 +129,9 @@ export function createDyadEngine(
|
|||||||
if (files?.length) {
|
if (files?.length) {
|
||||||
parsedBody.dyad_options = {
|
parsedBody.dyad_options = {
|
||||||
files,
|
files,
|
||||||
enable_lazy_edits: true,
|
enable_lazy_edits: options.dyadOptions.enableLazyEdits,
|
||||||
|
enable_smart_files_context:
|
||||||
|
options.dyadOptions.enableSmartFilesContext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export const UserSettingsSchema = z.object({
|
|||||||
maxChatTurnsInContext: z.number().optional(),
|
maxChatTurnsInContext: z.number().optional(),
|
||||||
enableProSaverMode: z.boolean().optional(),
|
enableProSaverMode: z.boolean().optional(),
|
||||||
enableProLazyEditsMode: z.boolean().optional(),
|
enableProLazyEditsMode: z.boolean().optional(),
|
||||||
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
||||||
// DEPRECATED.
|
// DEPRECATED.
|
||||||
runtimeMode: RuntimeModeSchema.optional(),
|
runtimeMode: RuntimeModeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -223,6 +223,21 @@ async function collectFiles(dir: string, baseDir: string): Promise<string[]> {
|
|||||||
return files;
|
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
|
* Format a file for inclusion in the codebase extract
|
||||||
*/
|
*/
|
||||||
@@ -230,18 +245,9 @@ async function formatFile(filePath: string, baseDir: string): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
const relativePath = path.relative(baseDir, filePath);
|
const relativePath = path.relative(baseDir, filePath);
|
||||||
|
|
||||||
// Skip large configuration files or generated code (just include the path)
|
if (isOmittedFile(relativePath)) {
|
||||||
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")
|
|
||||||
) {
|
|
||||||
return `<dyad-file path="${relativePath}">
|
return `<dyad-file path="${relativePath}">
|
||||||
// Contents omitted for brevity
|
${OMITTED_FILE_CONTENT}
|
||||||
</dyad-file>
|
</dyad-file>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
@@ -305,11 +311,13 @@ export async function extractCodebase(appPath: string): Promise<{
|
|||||||
|
|
||||||
// Get raw content for the files array
|
// Get raw content for the files array
|
||||||
const relativePath = path.relative(appPath, file);
|
const relativePath = path.relative(appPath, file);
|
||||||
const rawContent = await readFileWithCache(file);
|
const fileContent = isOmittedFile(relativePath)
|
||||||
if (rawContent !== null) {
|
? OMITTED_FILE_CONTENT
|
||||||
|
: await readFileWithCache(file);
|
||||||
|
if (fileContent !== null) {
|
||||||
filesArray.push({
|
filesArray.push({
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
content: rawContent,
|
content: fileContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user