Smart Context: deep (#1527)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Introduce a new "deep" Smart Context mode that supplies versioned files (by commit) to the engine, adds code search rendering, stores source commit hashes, improves search-replace recovery, and updates UI/tests. > > - **Smart Context (deep)**: > - Replace `conservative` with `deep`; limit context to ~200 turns; send `sourceCommitHash` per message. > - Build and pass `versioned_files` (hash-id map + per-message file refs) and `app_id` to engine. > - **DB**: > - Add `messages.source_commit_hash` (+ migration/snapshot). > - **Engine/Processing**: > - Retry Turbo Edits v2: first re-read then fallback to `dyad-write` if search-replace fails. > - Include provider options and versioned files in requests; add `getCurrentCommitHash`/`getFileAtCommit`. > - **UI**: > - Pro mode selector: new `deep` option; tooltips polish. > - Add `DyadCodeSearch` and `DyadCodeSearchResult` components; parser supports new tags. > - **Tests/E2E**: > - New `smart_context_deep` e2e; update snapshots to include `app_id` and deep mode; adjust Playwright timeout. > - Unit tests for versioned codebase context. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e3d3bffabb2bc6caf52103461f9d6f2d5ad39df8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
@@ -81,6 +81,11 @@ import { mcpManager } from "../utils/mcp_manager";
|
||||
import z from "zod";
|
||||
import { isTurboEditsV2Enabled } from "@/lib/schemas";
|
||||
import { AI_STREAMING_ERROR_MESSAGE_PREFIX } from "@/shared/texts";
|
||||
import { getCurrentCommitHash } from "../utils/git_utils";
|
||||
import {
|
||||
processChatMessagesWithVersionedFiles as getVersionedFiles,
|
||||
VersionedFiles as VersionedFiles,
|
||||
} from "../utils/versioned_codebase_context";
|
||||
|
||||
type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
|
||||
|
||||
@@ -407,6 +412,9 @@ ${componentSnippet}
|
||||
role: "assistant",
|
||||
content: "", // Start with empty content
|
||||
requestId: dyadRequestId,
|
||||
sourceCommitHash: await getCurrentCommitHash({
|
||||
path: getDyadAppPath(chat.app.path),
|
||||
}),
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -523,12 +531,20 @@ ${componentSnippet}
|
||||
const messageHistory = updatedChat.messages.map((message) => ({
|
||||
role: message.role as "user" | "assistant" | "system",
|
||||
content: message.content,
|
||||
sourceCommitHash: message.sourceCommitHash,
|
||||
}));
|
||||
|
||||
// For Dyad Pro + Deep Context, we set to 200 chat turns (+1)
|
||||
// this is to enable more cache hits. Practically, users should
|
||||
// rarely go over this limit because they will hit the model's
|
||||
// context window limit.
|
||||
//
|
||||
// Limit chat history based on maxChatTurnsInContext setting
|
||||
// We add 1 because the current prompt counts as a turn.
|
||||
const maxChatTurns =
|
||||
(settings.maxChatTurnsInContext || MAX_CHAT_TURNS_IN_CONTEXT) + 1;
|
||||
isEngineEnabled && settings.proSmartContextOption === "deep"
|
||||
? 201
|
||||
: (settings.maxChatTurnsInContext || MAX_CHAT_TURNS_IN_CONTEXT) + 1;
|
||||
|
||||
// If we need to limit the context, we take only the most recent turns
|
||||
let limitedMessageHistory = messageHistory;
|
||||
@@ -713,6 +729,11 @@ This conversation includes one or more image attachments. When the user uploads
|
||||
settings.selectedChatMode === "ask"
|
||||
? removeDyadTags(removeNonEssentialTags(msg.content))
|
||||
: removeNonEssentialTags(msg.content),
|
||||
providerOptions: {
|
||||
"dyad-engine": {
|
||||
sourceCommitHash: msg.sourceCommitHash,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let chatMessages: ModelMessage[] = [
|
||||
@@ -776,12 +797,22 @@ This conversation includes one or more image attachments. When the user uploads
|
||||
} else {
|
||||
logger.log("sending AI request");
|
||||
}
|
||||
let versionedFiles: VersionedFiles | undefined;
|
||||
if (isEngineEnabled && settings.proSmartContextOption === "deep") {
|
||||
versionedFiles = await getVersionedFiles({
|
||||
files,
|
||||
chatMessages,
|
||||
appPath,
|
||||
});
|
||||
}
|
||||
// Build provider options with correct Google/Vertex thinking config gating
|
||||
const providerOptions: Record<string, any> = {
|
||||
"dyad-engine": {
|
||||
dyadAppId: updatedChat.app.id,
|
||||
dyadRequestId,
|
||||
dyadDisableFiles,
|
||||
dyadFiles: files,
|
||||
dyadFiles: versionedFiles ? undefined : files,
|
||||
dyadVersionedFiles: versionedFiles,
|
||||
dyadMentionedApps: mentionedAppsCodebases.map(
|
||||
({ files, appName }) => ({
|
||||
appName,
|
||||
@@ -979,21 +1010,48 @@ This conversation includes one or more image attachments. When the user uploads
|
||||
settings.selectedChatMode !== "ask" &&
|
||||
isTurboEditsV2Enabled(settings)
|
||||
) {
|
||||
const issues = await dryRunSearchReplace({
|
||||
let issues = await dryRunSearchReplace({
|
||||
fullResponse,
|
||||
appPath: getDyadAppPath(updatedChat.app.path),
|
||||
});
|
||||
if (issues.length > 0) {
|
||||
|
||||
let searchReplaceFixAttempts = 0;
|
||||
const originalFullResponse = fullResponse;
|
||||
const previousAttempts: ModelMessage[] = [];
|
||||
while (
|
||||
issues.length > 0 &&
|
||||
searchReplaceFixAttempts < 2 &&
|
||||
!abortController.signal.aborted
|
||||
) {
|
||||
logger.warn(
|
||||
`Detected search-replace issues: ${issues.map((i) => i.error).join(", ")}`,
|
||||
`Detected search-replace issues (attempt #${searchReplaceFixAttempts + 1}): ${issues.map((i) => i.error).join(", ")}`,
|
||||
);
|
||||
const formattedSearchReplaceIssues = issues
|
||||
.map(({ filePath, error }) => {
|
||||
return `File path: ${filePath}\nError: ${error}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
const originalFullResponse = fullResponse;
|
||||
|
||||
fullResponse += `<dyad-output type="warning" message="Could not apply Turbo Edits properly for some of the files; re-generating code...">${formattedSearchReplaceIssues}</dyad-output>`;
|
||||
await processResponseChunkUpdate({
|
||||
fullResponse,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Attempting to fix search-replace issues, attempt #${searchReplaceFixAttempts + 1}`,
|
||||
);
|
||||
|
||||
const fixSearchReplacePrompt =
|
||||
searchReplaceFixAttempts === 0
|
||||
? `There was an issue with the following \`dyad-search-replace\` tags. Make sure you use \`dyad-read\` to read the latest version of the file and then trying to do search & replace again.`
|
||||
: `There was an issue with the following \`dyad-search-replace\` tags. Please fix the errors by generating the code changes using \`dyad-write\` tags instead.`;
|
||||
searchReplaceFixAttempts++;
|
||||
const userPrompt = {
|
||||
role: "user",
|
||||
content: `${fixSearchReplacePrompt}
|
||||
|
||||
${formattedSearchReplaceIssues}`,
|
||||
} as const;
|
||||
|
||||
const { fullStream: fixSearchReplaceStream } =
|
||||
await simpleStreamText({
|
||||
@@ -1001,16 +1059,13 @@ This conversation includes one or more image attachments. When the user uploads
|
||||
chatMessages: [
|
||||
...chatMessages,
|
||||
{ role: "assistant", content: originalFullResponse },
|
||||
{
|
||||
role: "user",
|
||||
content: `There was an issue with the following \`dyad-search-replace\` tags. Please fix them by generating the code changes using \`dyad-write\` tags instead.
|
||||
|
||||
${formattedSearchReplaceIssues}`,
|
||||
},
|
||||
...previousAttempts,
|
||||
userPrompt,
|
||||
],
|
||||
modelClient,
|
||||
files: files,
|
||||
});
|
||||
previousAttempts.push(userPrompt);
|
||||
const result = await processStreamChunks({
|
||||
fullStream: fixSearchReplaceStream,
|
||||
fullResponse,
|
||||
@@ -1019,6 +1074,16 @@ ${formattedSearchReplaceIssues}`,
|
||||
processResponseChunkUpdate,
|
||||
});
|
||||
fullResponse = result.fullResponse;
|
||||
previousAttempts.push({
|
||||
role: "assistant",
|
||||
content: removeNonEssentialTags(result.incrementalResponse),
|
||||
});
|
||||
|
||||
// Re-check for issues after the fix attempt
|
||||
issues = await dryRunSearchReplace({
|
||||
fullResponse: result.incrementalResponse,
|
||||
appPath: getDyadAppPath(updatedChat.app.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user