Fixes #1222 #1646 TODOs - [x] description? - [x] collect errors across all files for turbo edits - [x] be forgiving around whitespaces - [x] write e2e tests - [x] do more manual testing across different models <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Turbo Edits v2 search-replace flow with settings/UI selector, parser/renderer, dry-run validation + fallback, proposal integration, and comprehensive tests; updates licensing. > > - **Engine/Processing**: > - Add `dyad-search-replace` end-to-end: parsing (`getDyadSearchReplaceTags`), markdown rendering (`DyadSearchReplace`), and application (`applySearchReplace`) with dry-run validation and fallback to `dyad-write`. > - Inject Turbo Edits v2 system prompt; toggle via `isTurboEditsV2Enabled`; disable classic lazy edits when v2 is on. > - Include search-replace edits in proposals and full-response processing. > - **Settings/UI**: > - Introduce `proLazyEditsMode` (`off`|`v1`|`v2`) and helper selectors; update `ProModeSelector` with Turbo Edits and Smart Context selectors (`data-testid`s). > - **LLM/token flow**: > - Construct system prompt conditionally; update token counting and chat stream to validate and repair search-replace responses. > - **Tests**: > - Add unit tests for search-replace processor; e2e tests for Turbo Edits v2 and options; fixtures and snapshots. > - **Licensing/Docs**: > - Add `src/pro/LICENSE` (FSL 1.1 ALv2 future), update root `LICENSE` and README license section. > - **Tooling**: > - Update `.prettierignore`; enhance test helpers (selectors, path normalization, snapshot filtering). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7aefa02bfae2fe22a25c7d87f3c4c326f820f1e6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
375 lines
10 KiB
TypeScript
375 lines
10 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const SecretSchema = z.object({
|
|
value: z.string(),
|
|
encryptionType: z.enum(["electron-safe-storage", "plaintext"]).optional(),
|
|
});
|
|
export type Secret = z.infer<typeof SecretSchema>;
|
|
|
|
/**
|
|
* Zod schema for chat summary objects returned by the get-chats IPC
|
|
*/
|
|
export const ChatSummarySchema = z.object({
|
|
id: z.number(),
|
|
appId: z.number(),
|
|
title: z.string().nullable(),
|
|
createdAt: z.date(),
|
|
});
|
|
|
|
/**
|
|
* Type derived from the ChatSummarySchema
|
|
*/
|
|
export type ChatSummary = z.infer<typeof ChatSummarySchema>;
|
|
|
|
/**
|
|
* Zod schema for an array of chat summaries
|
|
*/
|
|
export const ChatSummariesSchema = z.array(ChatSummarySchema);
|
|
|
|
/**
|
|
* Zod schema for chat search result objects returned by the search-chats IPC
|
|
*/
|
|
export const ChatSearchResultSchema = z.object({
|
|
id: z.number(),
|
|
appId: z.number(),
|
|
title: z.string().nullable(),
|
|
createdAt: z.date(),
|
|
matchedMessageContent: z.string().nullable(),
|
|
});
|
|
|
|
/**
|
|
* Type derived from the ChatSearchResultSchema
|
|
*/
|
|
export type ChatSearchResult = z.infer<typeof ChatSearchResultSchema>;
|
|
|
|
export const ChatSearchResultsSchema = z.array(ChatSearchResultSchema);
|
|
|
|
// Zod schema for app search result objects returned by the search-app IPC
|
|
export const AppSearchResultSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
createdAt: z.date(),
|
|
matchedChatTitle: z.string().nullable(),
|
|
matchedChatMessage: z.string().nullable(),
|
|
});
|
|
|
|
// Type derived from AppSearchResultSchema
|
|
export type AppSearchResult = z.infer<typeof AppSearchResultSchema>;
|
|
|
|
export const AppSearchResultsSchema = z.array(AppSearchResultSchema);
|
|
|
|
const providers = [
|
|
"openai",
|
|
"anthropic",
|
|
"google",
|
|
"vertex",
|
|
"auto",
|
|
"openrouter",
|
|
"ollama",
|
|
"lmstudio",
|
|
"azure",
|
|
"xai",
|
|
"bedrock",
|
|
] as const;
|
|
|
|
export const cloudProviders = providers.filter(
|
|
(provider) => provider !== "ollama" && provider !== "lmstudio",
|
|
);
|
|
|
|
/**
|
|
* Zod schema for large language model configuration
|
|
*/
|
|
export const LargeLanguageModelSchema = z.object({
|
|
name: z.string(),
|
|
provider: z.string(),
|
|
customModelId: z.number().optional(),
|
|
});
|
|
|
|
/**
|
|
* Type derived from the LargeLanguageModelSchema
|
|
*/
|
|
export type LargeLanguageModel = z.infer<typeof LargeLanguageModelSchema>;
|
|
|
|
/**
|
|
* Zod schema for provider settings
|
|
* Regular providers use only apiKey. Vertex has additional optional fields.
|
|
*/
|
|
export const RegularProviderSettingSchema = z.object({
|
|
apiKey: SecretSchema.optional(),
|
|
});
|
|
|
|
export const AzureProviderSettingSchema = z.object({
|
|
apiKey: SecretSchema.optional(),
|
|
resourceName: z.string().optional(),
|
|
});
|
|
|
|
export const VertexProviderSettingSchema = z.object({
|
|
// We make this undefined so that it makes existing callsites easier.
|
|
apiKey: z.undefined(),
|
|
projectId: z.string().optional(),
|
|
location: z.string().optional(),
|
|
serviceAccountKey: SecretSchema.optional(),
|
|
});
|
|
|
|
export const ProviderSettingSchema = z.union([
|
|
// Must use more specific type first!
|
|
// Zod uses the first type that matches.
|
|
//
|
|
// We use passthrough as a hack because Azure and Vertex
|
|
// will match together since their required fields overlap.
|
|
//
|
|
// In addition, there may be future provider settings that
|
|
// we may want to preserve (e.g. user downgrades to older version)
|
|
// so doing passthrough keeps these extra fields.
|
|
AzureProviderSettingSchema.passthrough(),
|
|
VertexProviderSettingSchema.passthrough(),
|
|
RegularProviderSettingSchema.passthrough(),
|
|
]);
|
|
|
|
/**
|
|
* Type derived from the ProviderSettingSchema
|
|
*/
|
|
export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
|
|
export type RegularProviderSetting = z.infer<
|
|
typeof RegularProviderSettingSchema
|
|
>;
|
|
export type AzureProviderSetting = z.infer<typeof AzureProviderSettingSchema>;
|
|
export type VertexProviderSetting = z.infer<typeof VertexProviderSettingSchema>;
|
|
|
|
export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]);
|
|
export type RuntimeMode = z.infer<typeof RuntimeModeSchema>;
|
|
|
|
export const RuntimeMode2Schema = z.enum(["host", "docker"]);
|
|
export type RuntimeMode2 = z.infer<typeof RuntimeMode2Schema>;
|
|
|
|
export const ChatModeSchema = z.enum(["build", "ask", "agent"]);
|
|
export type ChatMode = z.infer<typeof ChatModeSchema>;
|
|
|
|
export const GitHubSecretsSchema = z.object({
|
|
accessToken: SecretSchema.nullable(),
|
|
});
|
|
export type GitHubSecrets = z.infer<typeof GitHubSecretsSchema>;
|
|
|
|
export const GithubUserSchema = z.object({
|
|
email: z.string(),
|
|
});
|
|
export type GithubUser = z.infer<typeof GithubUserSchema>;
|
|
|
|
export const SupabaseSchema = z.object({
|
|
accessToken: SecretSchema.optional(),
|
|
refreshToken: SecretSchema.optional(),
|
|
expiresIn: z.number().optional(),
|
|
tokenTimestamp: z.number().optional(),
|
|
});
|
|
export type Supabase = z.infer<typeof SupabaseSchema>;
|
|
|
|
export const NeonSchema = z.object({
|
|
accessToken: SecretSchema.optional(),
|
|
refreshToken: SecretSchema.optional(),
|
|
expiresIn: z.number().optional(),
|
|
tokenTimestamp: z.number().optional(),
|
|
});
|
|
export type Neon = z.infer<typeof NeonSchema>;
|
|
|
|
export const ExperimentsSchema = z.object({
|
|
// Deprecated
|
|
enableSupabaseIntegration: z.boolean().describe("DEPRECATED").optional(),
|
|
enableFileEditing: z.boolean().describe("DEPRECATED").optional(),
|
|
});
|
|
export type Experiments = z.infer<typeof ExperimentsSchema>;
|
|
|
|
export const DyadProBudgetSchema = z.object({
|
|
budgetResetAt: z.string(),
|
|
maxBudget: z.number(),
|
|
});
|
|
export type DyadProBudget = z.infer<typeof DyadProBudgetSchema>;
|
|
|
|
export const GlobPathSchema = z.object({
|
|
globPath: z.string(),
|
|
});
|
|
|
|
export type GlobPath = z.infer<typeof GlobPathSchema>;
|
|
|
|
export const AppChatContextSchema = z.object({
|
|
contextPaths: z.array(GlobPathSchema),
|
|
smartContextAutoIncludes: z.array(GlobPathSchema),
|
|
excludePaths: z.array(GlobPathSchema).optional(),
|
|
});
|
|
export type AppChatContext = z.infer<typeof AppChatContextSchema>;
|
|
|
|
export type ContextPathResult = GlobPath & {
|
|
files: number;
|
|
tokens: number;
|
|
};
|
|
|
|
export type ContextPathResults = {
|
|
contextPaths: ContextPathResult[];
|
|
smartContextAutoIncludes: ContextPathResult[];
|
|
excludePaths: ContextPathResult[];
|
|
};
|
|
|
|
export const ReleaseChannelSchema = z.enum(["stable", "beta"]);
|
|
export type ReleaseChannel = z.infer<typeof ReleaseChannelSchema>;
|
|
|
|
/**
|
|
* Zod schema for user settings
|
|
*/
|
|
export const UserSettingsSchema = z.object({
|
|
selectedModel: LargeLanguageModelSchema,
|
|
providerSettings: z.record(z.string(), ProviderSettingSchema),
|
|
githubUser: GithubUserSchema.optional(),
|
|
githubAccessToken: SecretSchema.optional(),
|
|
vercelAccessToken: SecretSchema.optional(),
|
|
supabase: SupabaseSchema.optional(),
|
|
neon: NeonSchema.optional(),
|
|
autoApproveChanges: z.boolean().optional(),
|
|
telemetryConsent: z.enum(["opted_in", "opted_out", "unset"]).optional(),
|
|
telemetryUserId: z.string().optional(),
|
|
hasRunBefore: z.boolean().optional(),
|
|
enableDyadPro: z.boolean().optional(),
|
|
experiments: ExperimentsSchema.optional(),
|
|
lastShownReleaseNotesVersion: z.string().optional(),
|
|
maxChatTurnsInContext: z.number().optional(),
|
|
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
|
|
enableProLazyEditsMode: z.boolean().optional(),
|
|
proLazyEditsMode: z.enum(["off", "v1", "v2"]).optional(),
|
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
|
enableProWebSearch: z.boolean().optional(),
|
|
proSmartContextOption: z.enum(["balanced", "conservative"]).optional(),
|
|
selectedTemplateId: z.string(),
|
|
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
|
selectedChatMode: ChatModeSchema.optional(),
|
|
acceptedCommunityCode: z.boolean().optional(),
|
|
|
|
enableAutoFixProblems: z.boolean().optional(),
|
|
enableNativeGit: z.boolean().optional(),
|
|
enableAutoUpdate: z.boolean(),
|
|
releaseChannel: ReleaseChannelSchema,
|
|
runtimeMode2: RuntimeMode2Schema.optional(),
|
|
customNodePath: z.string().optional().nullable(),
|
|
|
|
////////////////////////////////
|
|
// E2E TESTING ONLY.
|
|
////////////////////////////////
|
|
isTestMode: z.boolean().optional(),
|
|
|
|
////////////////////////////////
|
|
// DEPRECATED.
|
|
////////////////////////////////
|
|
enableProSaverMode: z.boolean().optional(),
|
|
dyadProBudget: DyadProBudgetSchema.optional(),
|
|
runtimeMode: RuntimeModeSchema.optional(),
|
|
});
|
|
|
|
/**
|
|
* Type derived from the UserSettingsSchema
|
|
*/
|
|
export type UserSettings = z.infer<typeof UserSettingsSchema>;
|
|
|
|
export function isDyadProEnabled(settings: UserSettings): boolean {
|
|
return settings.enableDyadPro === true && hasDyadProKey(settings);
|
|
}
|
|
|
|
export function hasDyadProKey(settings: UserSettings): boolean {
|
|
return !!settings.providerSettings?.auto?.apiKey?.value;
|
|
}
|
|
|
|
export function isTurboEditsV2Enabled(settings: UserSettings): boolean {
|
|
return Boolean(
|
|
isDyadProEnabled(settings) &&
|
|
settings.enableProLazyEditsMode === true &&
|
|
settings.proLazyEditsMode === "v2",
|
|
);
|
|
}
|
|
|
|
// Define interfaces for the props
|
|
export interface SecurityRisk {
|
|
type: "warning" | "danger";
|
|
title: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface FileChange {
|
|
name: string;
|
|
path: string;
|
|
summary: string;
|
|
type: "write" | "rename" | "delete";
|
|
isServerFunction: boolean;
|
|
}
|
|
|
|
export interface CodeProposal {
|
|
type: "code-proposal";
|
|
title: string;
|
|
securityRisks: SecurityRisk[];
|
|
filesChanged: FileChange[];
|
|
packagesAdded: string[];
|
|
sqlQueries: SqlQuery[];
|
|
}
|
|
|
|
export type SuggestedAction =
|
|
| RestartAppAction
|
|
| SummarizeInNewChatAction
|
|
| RefactorFileAction
|
|
| WriteCodeProperlyAction
|
|
| RebuildAction
|
|
| RestartAction
|
|
| RefreshAction
|
|
| KeepGoingAction;
|
|
|
|
export interface RestartAppAction {
|
|
id: "restart-app";
|
|
}
|
|
|
|
export interface SummarizeInNewChatAction {
|
|
id: "summarize-in-new-chat";
|
|
}
|
|
|
|
export interface WriteCodeProperlyAction {
|
|
id: "write-code-properly";
|
|
}
|
|
|
|
export interface RefactorFileAction {
|
|
id: "refactor-file";
|
|
path: string;
|
|
}
|
|
|
|
export interface RebuildAction {
|
|
id: "rebuild";
|
|
}
|
|
|
|
export interface RestartAction {
|
|
id: "restart";
|
|
}
|
|
|
|
export interface RefreshAction {
|
|
id: "refresh";
|
|
}
|
|
|
|
export interface KeepGoingAction {
|
|
id: "keep-going";
|
|
}
|
|
|
|
export interface ActionProposal {
|
|
type: "action-proposal";
|
|
actions: SuggestedAction[];
|
|
}
|
|
|
|
export interface TipProposal {
|
|
type: "tip-proposal";
|
|
title: string;
|
|
description: string;
|
|
}
|
|
|
|
export type Proposal = CodeProposal | ActionProposal | TipProposal;
|
|
|
|
export interface ProposalResult {
|
|
proposal: Proposal;
|
|
chatId: number;
|
|
messageId: number;
|
|
}
|
|
|
|
export interface SqlQuery {
|
|
content: string;
|
|
description?: string;
|
|
}
|