Skip to content

Commit a8f3c97

Browse files
authored
Turbo edits v2 (dyad-sh#1653)
Fixes dyad-sh#1222 dyad-sh#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 7aefa02. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8a3bc53 commit a8f3c97

File tree

36 files changed

+2537
-72
lines changed

36 files changed

+2537
-72
lines changed

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ coverage
44
# generated files
55
drizzle/
66
**/pnpm-lock.yaml
7-
**/snapshots/**
7+
**/snapshots/**
8+
# test fixtures
9+
e2e-tests/fixtures/**

LICENSE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Portions of this software are licensed as follows:
2+
3+
* All content that resides under the "src/pro/" directory of this repository, if that directory exists, is licensed under the license defined in "src/pro/LICENSE".
4+
* Content outside of the above mentioned directories or restrictions above is available under the Apache 2 license as defined below.
15

26
Apache License
37
Version 2.0, January 2004

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ Join our growing community of AI app builders on **Reddit**: [r/dyadbuilders](ht
2727
**Dyad** is open-source (Apache 2.0 licensed).
2828

2929
If you're interested in contributing to dyad, please read our [contributing](./CONTRIBUTING.md) doc.
30+
31+
## License
32+
33+
- All the code in this repo outside of `src/pro` is open-source and licensed under Apache 2.0 - see [LICENSE](./LICENSE).
34+
- All the code in this repo within `src/pro` is fair-source and licensed under [Functional Source License 1.1 Apache 2.0](https://fsl.software/) - see [LICENSE](./src/pro/LICENSE).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Example with turbo edit v2
2+
<dyad-search-replace path="src/pages/Index.tsx">
3+
<<<<<<< SEARCH
4+
// Intentionally DO NOT MATCH ANYTHING TO TRIGGER FALLBACK
5+
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
6+
=======
7+
<h1 className="text-4xl font-bold mb-4">Welcome to the UPDATED App</h1>
8+
>>>>>>> REPLACE
9+
</dyad-search-replace>
10+
End of turbo edit
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Example with turbo edit v2
2+
<dyad-search-replace path="src/pages/Index.tsx">
3+
<<<<<<< SEARCH
4+
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
5+
=======
6+
<h1 className="text-4xl font-bold mb-4">Welcome to the UPDATED App</h1>
7+
>>>>>>> REPLACE
8+
</dyad-search-replace>
9+
End of turbo edit

e2e-tests/helpers/test_helper.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,23 @@ class ProModesDialog {
7373

7474
async setSmartContextMode(mode: "balanced" | "off" | "conservative") {
7575
await this.page
76+
.getByTestId("smart-context-selector")
7677
.getByRole("button", {
7778
name: mode.charAt(0).toUpperCase() + mode.slice(1),
7879
})
7980
.click();
8081
}
8182

82-
async toggleTurboEdits() {
83-
await this.page.getByRole("switch", { name: "Turbo Edits" }).click();
83+
async setTurboEditsMode(mode: "off" | "classic" | "search-replace") {
84+
await this.page
85+
.getByTestId("turbo-edits-selector")
86+
.getByRole("button", {
87+
name:
88+
mode === "search-replace"
89+
? "Search & replace"
90+
: mode.charAt(0).toUpperCase() + mode.slice(1),
91+
})
92+
.click();
8493
}
8594
}
8695

@@ -362,7 +371,7 @@ export class PageObject {
362371
await expect(this.page.getByRole("dialog")).toMatchAriaSnapshot();
363372
}
364373

365-
async snapshotAppFiles({ name }: { name: string }) {
374+
async snapshotAppFiles({ name, files }: { name: string; files?: string[] }) {
366375
const currentAppName = await this.getCurrentAppName();
367376
if (!currentAppName) {
368377
throw new Error("No app selected");
@@ -374,10 +383,17 @@ export class PageObject {
374383
}
375384

376385
await expect(() => {
377-
const filesData = generateAppFilesSnapshotData(appPath, appPath);
386+
let filesData = generateAppFilesSnapshotData(appPath, appPath);
378387

379388
// Sort by relative path to ensure deterministic output
380389
filesData.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
390+
if (files) {
391+
filesData = filesData.filter((file) =>
392+
files.some(
393+
(f) => normalizePath(f) === normalizePath(file.relativePath),
394+
),
395+
);
396+
}
381397

382398
const snapshotContent = filesData
383399
.map(
@@ -1232,3 +1248,7 @@ function prettifyDump(
12321248
})
12331249
.join("\n\n");
12341250
}
1251+
1252+
function normalizePath(path: string): string {
1253+
return path.replace(/\\/g, "/");
1254+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"selectedModel": {
3+
"name": "auto",
4+
"provider": "auto"
5+
},
6+
"providerSettings": {
7+
"auto": {
8+
"apiKey": {
9+
"value": "testdyadkey",
10+
"encryptionType": "plaintext"
11+
}
12+
}
13+
},
14+
"telemetryConsent": "unset",
15+
"telemetryUserId": "[UUID]",
16+
"hasRunBefore": true,
17+
"enableDyadPro": true,
18+
"experiments": {},
19+
"lastShownReleaseNotesVersion": "[scrubbed]",
20+
"enableProLazyEditsMode": true,
21+
"enableProSmartFilesContextMode": true,
22+
"selectedTemplateId": "react",
23+
"selectedChatMode": "build",
24+
"enableAutoFixProblems": false,
25+
"enableAutoUpdate": true,
26+
"releaseChannel": "stable",
27+
"isTestMode": true
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"selectedModel": {
3+
"name": "auto",
4+
"provider": "auto"
5+
},
6+
"providerSettings": {
7+
"auto": {
8+
"apiKey": {
9+
"value": "testdyadkey",
10+
"encryptionType": "plaintext"
11+
}
12+
}
13+
},
14+
"telemetryConsent": "unset",
15+
"telemetryUserId": "[UUID]",
16+
"hasRunBefore": true,
17+
"enableDyadPro": true,
18+
"experiments": {},
19+
"lastShownReleaseNotesVersion": "[scrubbed]",
20+
"enableProLazyEditsMode": true,
21+
"proLazyEditsMode": "v1",
22+
"enableProSmartFilesContextMode": true,
23+
"selectedTemplateId": "react",
24+
"selectedChatMode": "build",
25+
"enableAutoFixProblems": false,
26+
"enableAutoUpdate": true,
27+
"releaseChannel": "stable",
28+
"isTestMode": true
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"selectedModel": {
3+
"name": "auto",
4+
"provider": "auto"
5+
},
6+
"providerSettings": {
7+
"auto": {
8+
"apiKey": {
9+
"value": "testdyadkey",
10+
"encryptionType": "plaintext"
11+
}
12+
}
13+
},
14+
"telemetryConsent": "unset",
15+
"telemetryUserId": "[UUID]",
16+
"hasRunBefore": true,
17+
"enableDyadPro": true,
18+
"experiments": {},
19+
"lastShownReleaseNotesVersion": "[scrubbed]",
20+
"enableProLazyEditsMode": true,
21+
"proLazyEditsMode": "v2",
22+
"enableProSmartFilesContextMode": true,
23+
"selectedTemplateId": "react",
24+
"selectedChatMode": "build",
25+
"enableAutoFixProblems": false,
26+
"enableAutoUpdate": true,
27+
"releaseChannel": "stable",
28+
"isTestMode": true
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"selectedModel": {
3+
"name": "auto",
4+
"provider": "auto"
5+
},
6+
"providerSettings": {
7+
"auto": {
8+
"apiKey": {
9+
"value": "testdyadkey",
10+
"encryptionType": "plaintext"
11+
}
12+
}
13+
},
14+
"telemetryConsent": "unset",
15+
"telemetryUserId": "[UUID]",
16+
"hasRunBefore": true,
17+
"enableDyadPro": true,
18+
"experiments": {},
19+
"lastShownReleaseNotesVersion": "[scrubbed]",
20+
"enableProLazyEditsMode": false,
21+
"proLazyEditsMode": "off",
22+
"enableProSmartFilesContextMode": true,
23+
"selectedTemplateId": "react",
24+
"selectedChatMode": "build",
25+
"enableAutoFixProblems": false,
26+
"enableAutoUpdate": true,
27+
"releaseChannel": "stable",
28+
"isTestMode": true
29+
}

0 commit comments

Comments
 (0)