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:
1
drizzle/0016_petite_thanos.sql
Normal file
1
drizzle/0016_petite_thanos.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `messages` ADD `source_commit_hash` text;
|
||||||
760
drizzle/meta/0016_snapshot.json
Normal file
760
drizzle/meta/0016_snapshot.json
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "c0a49147-ac92-4046-afe8-42f20df9314b",
|
||||||
|
"prevId": "41549b62-b247-48d5-90e1-6bfc70f02040",
|
||||||
|
"tables": {
|
||||||
|
"apps": {
|
||||||
|
"name": "apps",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"github_org": {
|
||||||
|
"name": "github_org",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"github_repo": {
|
||||||
|
"name": "github_repo",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"github_branch": {
|
||||||
|
"name": "github_branch",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"supabase_project_id": {
|
||||||
|
"name": "supabase_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"supabase_parent_project_id": {
|
||||||
|
"name": "supabase_parent_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_project_id": {
|
||||||
|
"name": "neon_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_development_branch_id": {
|
||||||
|
"name": "neon_development_branch_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_preview_branch_id": {
|
||||||
|
"name": "neon_preview_branch_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_project_id": {
|
||||||
|
"name": "vercel_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_project_name": {
|
||||||
|
"name": "vercel_project_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_team_id": {
|
||||||
|
"name": "vercel_team_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_deployment_url": {
|
||||||
|
"name": "vercel_deployment_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"install_command": {
|
||||||
|
"name": "install_command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"start_command": {
|
||||||
|
"name": "start_command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"chat_context": {
|
||||||
|
"name": "chat_context",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_favorite": {
|
||||||
|
"name": "is_favorite",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"name": "chats",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"name": "app_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"initial_commit_hash": {
|
||||||
|
"name": "initial_commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"chats_app_id_apps_id_fk": {
|
||||||
|
"name": "chats_app_id_apps_id_fk",
|
||||||
|
"tableFrom": "chats",
|
||||||
|
"tableTo": "apps",
|
||||||
|
"columnsFrom": [
|
||||||
|
"app_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"language_model_providers": {
|
||||||
|
"name": "language_model_providers",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"api_base_url": {
|
||||||
|
"name": "api_base_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_var_name": {
|
||||||
|
"name": "env_var_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"language_models": {
|
||||||
|
"name": "language_models",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"api_name": {
|
||||||
|
"name": "api_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"builtin_provider_id": {
|
||||||
|
"name": "builtin_provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_provider_id": {
|
||||||
|
"name": "custom_provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"max_output_tokens": {
|
||||||
|
"name": "max_output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"context_window": {
|
||||||
|
"name": "context_window",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"language_models_custom_provider_id_language_model_providers_id_fk": {
|
||||||
|
"name": "language_models_custom_provider_id_language_model_providers_id_fk",
|
||||||
|
"tableFrom": "language_models",
|
||||||
|
"tableTo": "language_model_providers",
|
||||||
|
"columnsFrom": [
|
||||||
|
"custom_provider_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"mcp_servers": {
|
||||||
|
"name": "mcp_servers",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"name": "transport",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"name": "command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"name": "args",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_json": {
|
||||||
|
"name": "env_json",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "0"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"mcp_tool_consents": {
|
||||||
|
"name": "mcp_tool_consents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"server_id": {
|
||||||
|
"name": "server_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_name": {
|
||||||
|
"name": "tool_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"consent": {
|
||||||
|
"name": "consent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'ask'"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"uniq_mcp_consent": {
|
||||||
|
"name": "uniq_mcp_consent",
|
||||||
|
"columns": [
|
||||||
|
"server_id",
|
||||||
|
"tool_name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"mcp_tool_consents_server_id_mcp_servers_id_fk": {
|
||||||
|
"name": "mcp_tool_consents_server_id_mcp_servers_id_fk",
|
||||||
|
"tableFrom": "mcp_tool_consents",
|
||||||
|
"tableTo": "mcp_servers",
|
||||||
|
"columnsFrom": [
|
||||||
|
"server_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"name": "messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"chat_id": {
|
||||||
|
"name": "chat_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"approval_state": {
|
||||||
|
"name": "approval_state",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source_commit_hash": {
|
||||||
|
"name": "source_commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"commit_hash": {
|
||||||
|
"name": "commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"messages_chat_id_chats_id_fk": {
|
||||||
|
"name": "messages_chat_id_chats_id_fk",
|
||||||
|
"tableFrom": "messages",
|
||||||
|
"tableTo": "chats",
|
||||||
|
"columnsFrom": [
|
||||||
|
"chat_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"name": "prompts",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"name": "versions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"name": "app_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"commit_hash": {
|
||||||
|
"name": "commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_db_timestamp": {
|
||||||
|
"name": "neon_db_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"versions_app_commit_unique": {
|
||||||
|
"name": "versions_app_commit_unique",
|
||||||
|
"columns": [
|
||||||
|
"app_id",
|
||||||
|
"commit_hash"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"versions_app_id_apps_id_fk": {
|
||||||
|
"name": "versions_app_id_apps_id_fk",
|
||||||
|
"tableFrom": "versions",
|
||||||
|
"tableTo": "apps",
|
||||||
|
"columnsFrom": [
|
||||||
|
"app_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,6 +113,13 @@
|
|||||||
"when": 1760474402750,
|
"when": 1760474402750,
|
||||||
"tag": "0015_complete_old_lace",
|
"tag": "0015_complete_old_lace",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1762297039106,
|
||||||
|
"tag": "0016_petite_thanos",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9,23 +9,6 @@ testSkipIfWindows("send message to engine", async ({ po }) => {
|
|||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
testSkipIfWindows(
|
|
||||||
"send message to engine - smart context conservative",
|
|
||||||
async ({ po }) => {
|
|
||||||
await po.setUpDyadPro();
|
|
||||||
const proModesDialog = await po.openProModesDialog({
|
|
||||||
location: "home-chat-input-container",
|
|
||||||
});
|
|
||||||
await proModesDialog.setSmartContextMode("conservative");
|
|
||||||
await proModesDialog.close();
|
|
||||||
await po.selectModel({ provider: "Google", model: "Gemini 2.5 Pro" });
|
|
||||||
await po.sendPrompt("[dump] tc=turbo-edits");
|
|
||||||
|
|
||||||
await po.snapshotServerDump("request");
|
|
||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testSkipIfWindows("send message to engine - openai gpt-5", async ({ po }) => {
|
testSkipIfWindows("send message to engine - openai gpt-5", async ({ po }) => {
|
||||||
await po.setUpDyadPro();
|
await po.setUpDyadPro();
|
||||||
// By default, it's using auto which points to Flash 2.5 and doesn't
|
// By default, it's using auto which points to Flash 2.5 and doesn't
|
||||||
|
|||||||
3
e2e-tests/fixtures/engine/read-index.md
Normal file
3
e2e-tests/fixtures/engine/read-index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Read the index page:
|
||||||
|
<dyad-read path="src/pages/Index.tsx"></dyad-read>
|
||||||
|
Done.
|
||||||
4
e2e-tests/fixtures/engine/update-index-1.md
Normal file
4
e2e-tests/fixtures/engine/update-index-1.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
First read
|
||||||
|
<dyad-write path="src/pages/Index.tsx" description="replace file">
|
||||||
|
// this file has been replaced
|
||||||
|
</dyad-write>
|
||||||
@@ -71,7 +71,7 @@ class ProModesDialog {
|
|||||||
public close: () => Promise<void>,
|
public close: () => Promise<void>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async setSmartContextMode(mode: "balanced" | "off" | "conservative") {
|
async setSmartContextMode(mode: "balanced" | "off" | "deep") {
|
||||||
await this.page
|
await this.page
|
||||||
.getByTestId("smart-context-selector")
|
.getByTestId("smart-context-selector")
|
||||||
.getByRole("button", {
|
.getByRole("button", {
|
||||||
|
|||||||
18
e2e-tests/smart_context_deep.spec.ts
Normal file
18
e2e-tests/smart_context_deep.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { testSkipIfWindows } from "./helpers/test_helper";
|
||||||
|
|
||||||
|
testSkipIfWindows("smart context deep - read write read", async ({ po }) => {
|
||||||
|
await po.setUpDyadPro({ autoApprove: true });
|
||||||
|
const proModesDialog = await po.openProModesDialog({
|
||||||
|
location: "home-chat-input-container",
|
||||||
|
});
|
||||||
|
await proModesDialog.setSmartContextMode("deep");
|
||||||
|
await proModesDialog.close();
|
||||||
|
|
||||||
|
await po.sendPrompt("tc=read-index");
|
||||||
|
await po.sendPrompt("tc=update-index-1");
|
||||||
|
await po.sendPrompt("tc=read-index");
|
||||||
|
await po.sendPrompt("[dump]");
|
||||||
|
|
||||||
|
await po.snapshotServerDump("request");
|
||||||
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
|
});
|
||||||
@@ -10,6 +10,6 @@ test("switching smart context mode saves the right setting", async ({ po }) => {
|
|||||||
await po.snapshotSettings();
|
await po.snapshotSettings();
|
||||||
await proModesDialog.setSmartContextMode("off");
|
await proModesDialog.setSmartContextMode("off");
|
||||||
await po.snapshotSettings();
|
await po.snapshotSettings();
|
||||||
await proModesDialog.setSmartContextMode("conservative");
|
await proModesDialog.setSmartContextMode("deep");
|
||||||
await po.snapshotSettings();
|
await po.snapshotSettings();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -69,7 +69,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -57,7 +57,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": false,
|
"enable_smart_files_context": false,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -115,7 +115,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": false,
|
"enable_smart_files_context": false,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -414,7 +414,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": false,
|
"enable_smart_files_context": false,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -409,7 +409,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -409,7 +409,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,10 @@
|
|||||||
- paragraph: "[dump] tc=turbo-edits"
|
- paragraph: "[dump] tc=turbo-edits"
|
||||||
- paragraph: "[[dyad-dump-path=*]]"
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
- img
|
- img
|
||||||
- text: less than a minute ago
|
- text: less than a minute ago
|
||||||
|
- button "Request ID":
|
||||||
|
- img
|
||||||
- button "Retry":
|
- button "Retry":
|
||||||
- img
|
- img
|
||||||
File diff suppressed because one or more lines are too long
@@ -414,7 +414,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -414,7 +414,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -415,6 +415,7 @@
|
|||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced",
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 2,
|
||||||
"mentioned_apps": [
|
"mentioned_apps": [
|
||||||
{
|
{
|
||||||
"appName": "minimal-with-ai-rules",
|
"appName": "minimal-with-ai-rules",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
- paragraph: tc=read-index
|
||||||
|
- paragraph: "Read the index page:"
|
||||||
|
- img
|
||||||
|
- text: Index.tsx Read src/pages/Index.tsx
|
||||||
|
- paragraph: Done.
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: Approved
|
||||||
|
- img
|
||||||
|
- text: less than a minute ago
|
||||||
|
- button "Request ID":
|
||||||
|
- img
|
||||||
|
- paragraph: tc=update-index-1
|
||||||
|
- paragraph: First read
|
||||||
|
- img
|
||||||
|
- text: Index.tsx
|
||||||
|
- button "Edit":
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: "src/pages/Index.tsx Summary: replace file"
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: Approved
|
||||||
|
- img
|
||||||
|
- text: less than a minute ago
|
||||||
|
- img
|
||||||
|
- text: wrote 1 file(s)
|
||||||
|
- button "Request ID":
|
||||||
|
- img
|
||||||
|
- paragraph: tc=read-index
|
||||||
|
- paragraph: "Read the index page:"
|
||||||
|
- img
|
||||||
|
- text: Index.tsx Read src/pages/Index.tsx
|
||||||
|
- paragraph: Done.
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: Approved
|
||||||
|
- img
|
||||||
|
- text: less than a minute ago
|
||||||
|
- button "Request ID":
|
||||||
|
- img
|
||||||
|
- paragraph: "[dump]"
|
||||||
|
- paragraph: "[[dyad-dump-path=*]]"
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- img
|
||||||
|
- text: Approved
|
||||||
|
- img
|
||||||
|
- text: less than a minute ago
|
||||||
|
- button "Request ID":
|
||||||
|
- img
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
File diff suppressed because one or more lines are too long
@@ -19,7 +19,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
"proSmartContextOption": "conservative",
|
"proSmartContextOption": "deep",
|
||||||
"selectedTemplateId": "react",
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
|
|||||||
@@ -422,7 +422,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -430,7 +430,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -438,7 +438,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": true,
|
"enable_lazy_edits": true,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -414,7 +414,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": false,
|
"enable_lazy_edits": false,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -18,7 +18,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"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.\n \nFile path: src/pages/Index.tsx\nError: Unable to apply search-replace to file"
|
"content": "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.\n \nFile path: src/pages/Index.tsx\nError: Unable to apply search-replace to file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "<dyad-read path=\"src/pages/Index.tsx\"></dyad-read>\n\n<dyad-search-replace path=\"src/pages/Index.tsx\">\n<<<<<<< SEARCH\n // STILL Intentionally DO NOT MATCH ANYTHING TO TRIGGER FALLBACK\n <h1 className=\"text-4xl font-bold mb-4\">Welcome to Your Blank App</h1>\n=======\n <h1 className=\"text-4xl font-bold mb-4\">Welcome to the UPDATED App</h1>\n>>>>>>> REPLACE\n</dyad-search-replace>\n\n\n[[dyad-dump-path=*]]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "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.\n \nFile path: src/pages/Index.tsx\nError: Unable to apply search-replace to file"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stream": true,
|
"stream": true,
|
||||||
@@ -422,7 +430,8 @@
|
|||||||
],
|
],
|
||||||
"enable_lazy_edits": false,
|
"enable_lazy_edits": false,
|
||||||
"enable_smart_files_context": true,
|
"enable_smart_files_context": true,
|
||||||
"smart_context_mode": "balanced"
|
"smart_context_mode": "balanced",
|
||||||
|
"app_id": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const config: PlaywrightTestConfig = {
|
|||||||
testDir: "./e2e-tests",
|
testDir: "./e2e-tests",
|
||||||
workers: 1,
|
workers: 1,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
timeout: process.env.CI ? 180_000 : 30_000,
|
timeout: process.env.CI ? 180_000 : 45_000,
|
||||||
// Use a custom snapshot path template because Playwright's default
|
// Use a custom snapshot path template because Playwright's default
|
||||||
// is platform-specific which isn't necessary for Dyad e2e tests
|
// is platform-specific which isn't necessary for Dyad e2e tests
|
||||||
// which should be platform agnostic (we don't do screenshots; only textual diffs).
|
// which should be platform agnostic (we don't do screenshots; only textual diffs).
|
||||||
|
|||||||
976
src/__tests__/versioned_codebase_context.test.ts
Normal file
976
src/__tests__/versioned_codebase_context.test.ts
Normal file
@@ -0,0 +1,976 @@
|
|||||||
|
import {
|
||||||
|
parseFilesFromMessage,
|
||||||
|
processChatMessagesWithVersionedFiles,
|
||||||
|
} from "@/ipc/utils/versioned_codebase_context";
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import type { ModelMessage } from "@ai-sdk/provider-utils";
|
||||||
|
import type { CodebaseFile } from "@/utils/codebase";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
// Mock git_utils
|
||||||
|
vi.mock("@/ipc/utils/git_utils", () => ({
|
||||||
|
getFileAtCommit: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock electron-log
|
||||||
|
vi.mock("electron-log", () => ({
|
||||||
|
default: {
|
||||||
|
scope: () => ({
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("parseFilesFromMessage", () => {
|
||||||
|
describe("dyad-read tags", () => {
|
||||||
|
it("should parse a single dyad-read tag", () => {
|
||||||
|
const input = '<dyad-read path="src/components/Button.tsx"></dyad-read>';
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual(["src/components/Button.tsx"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiple dyad-read tags", () => {
|
||||||
|
const input = `
|
||||||
|
<dyad-read path="src/components/Button.tsx"></dyad-read>
|
||||||
|
<dyad-read path="src/utils/helpers.ts"></dyad-read>
|
||||||
|
<dyad-read path="src/styles/main.css"></dyad-read>
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
"src/styles/main.css",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim whitespace from file paths in dyad-read tags", () => {
|
||||||
|
const input =
|
||||||
|
'<dyad-read path=" src/components/Button.tsx "></dyad-read>';
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual(["src/components/Button.tsx"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip empty path attributes", () => {
|
||||||
|
const input = `
|
||||||
|
<dyad-read path="src/components/Button.tsx"></dyad-read>
|
||||||
|
<dyad-read path=""></dyad-read>
|
||||||
|
<dyad-read path="src/utils/helpers.ts"></dyad-read>
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file paths with special characters", () => {
|
||||||
|
const input =
|
||||||
|
'<dyad-read path="src/components/@special/Button-v2.tsx"></dyad-read>';
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual(["src/components/@special/Button-v2.tsx"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("dyad-code-search-result tags", () => {
|
||||||
|
it("should parse a single file from dyad-code-search-result", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual(["src/components/Button.tsx"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiple files from dyad-code-search-result", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
src/components/Input.tsx
|
||||||
|
src/utils/helpers.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim whitespace from each line", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
src/components/Input.tsx
|
||||||
|
src/utils/helpers.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip empty lines in dyad-code-search-result", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
|
||||||
|
src/components/Input.tsx
|
||||||
|
|
||||||
|
|
||||||
|
src/utils/helpers.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip lines that look like tags (starting with < or >)", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
<some-tag>
|
||||||
|
src/components/Input.tsx
|
||||||
|
>some-line
|
||||||
|
src/utils/helpers.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple dyad-code-search-result tags", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
src/components/Input.tsx
|
||||||
|
</dyad-code-search-result>
|
||||||
|
|
||||||
|
Some text in between
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/utils/helpers.ts
|
||||||
|
src/styles/main.css
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
"src/styles/main.css",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mixed tags", () => {
|
||||||
|
it("should parse both dyad-read and dyad-code-search-result tags", () => {
|
||||||
|
const input = `
|
||||||
|
<dyad-read path="src/config/app.ts"></dyad-read>
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
src/components/Input.tsx
|
||||||
|
</dyad-code-search-result>
|
||||||
|
|
||||||
|
<dyad-read path="src/utils/helpers.ts"></dyad-read>
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/config/app.ts",
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/components/Input.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deduplicate file paths", () => {
|
||||||
|
const input = `
|
||||||
|
<dyad-read path="src/components/Button.tsx"></dyad-read>
|
||||||
|
<dyad-read path="src/components/Button.tsx"></dyad-read>
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/components/Button.tsx
|
||||||
|
src/utils/helpers.ts
|
||||||
|
</dyad-code-search-result>
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Button.tsx",
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex real-world example", () => {
|
||||||
|
const input = `
|
||||||
|
Here's what I found:
|
||||||
|
|
||||||
|
<dyad-read path="src/components/Header.tsx"></dyad-read>
|
||||||
|
|
||||||
|
I also searched for related files:
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/components/Header.tsx
|
||||||
|
src/components/Footer.tsx
|
||||||
|
src/styles/layout.css
|
||||||
|
</dyad-code-search-result>
|
||||||
|
|
||||||
|
Let me also check the config:
|
||||||
|
|
||||||
|
<dyad-read path="src/config/site.ts"></dyad-read>
|
||||||
|
|
||||||
|
And finally:
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/utils/navigation.ts
|
||||||
|
src/utils/theme.ts
|
||||||
|
</dyad-code-search-result>
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/components/Header.tsx",
|
||||||
|
"src/components/Footer.tsx",
|
||||||
|
"src/styles/layout.css",
|
||||||
|
"src/config/site.ts",
|
||||||
|
"src/utils/navigation.ts",
|
||||||
|
"src/utils/theme.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("should return empty array for empty string", () => {
|
||||||
|
const input = "";
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no tags present", () => {
|
||||||
|
const input = "This is just some regular text without any tags.";
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle malformed tags gracefully", () => {
|
||||||
|
const input = `
|
||||||
|
<dyad-read path="src/file1.ts"
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/file2.ts
|
||||||
|
`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
// Should not match unclosed tags
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nested angle brackets in file paths", () => {
|
||||||
|
const input =
|
||||||
|
'<dyad-read path="src/components/Generic<T>.tsx"></dyad-read>';
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual(["src/components/Generic<T>.tsx"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve file path case sensitivity", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
src/Components/Button.tsx
|
||||||
|
src/components/button.tsx
|
||||||
|
SRC/COMPONENTS/BUTTON.TSX
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"src/Components/Button.tsx",
|
||||||
|
"src/components/button.tsx",
|
||||||
|
"SRC/COMPONENTS/BUTTON.TSX",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle very long file paths", () => {
|
||||||
|
const longPath =
|
||||||
|
"src/very/deeply/nested/directory/structure/with/many/levels/components/Button.tsx";
|
||||||
|
const input = `<dyad-read path="${longPath}"></dyad-read>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([longPath]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle file paths with dots", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
./src/components/Button.tsx
|
||||||
|
../utils/helpers.ts
|
||||||
|
../../config/app.config.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"./src/components/Button.tsx",
|
||||||
|
"../utils/helpers.ts",
|
||||||
|
"../../config/app.config.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle absolute paths", () => {
|
||||||
|
const input = `<dyad-code-search-result>
|
||||||
|
/absolute/path/to/file.tsx
|
||||||
|
/another/absolute/path.ts
|
||||||
|
</dyad-code-search-result>`;
|
||||||
|
const result = parseFilesFromMessage(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"/absolute/path/to/file.tsx",
|
||||||
|
"/another/absolute/path.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("processChatMessagesWithVersionedFiles", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clear all mocks before each test
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to compute SHA-256 hash
|
||||||
|
const hashContent = (content: string): string => {
|
||||||
|
return crypto.createHash("sha256").update(content).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("basic functionality", () => {
|
||||||
|
it("should process files parameter and create fileIdToContent and fileReferences", async () => {
|
||||||
|
const files: CodebaseFile[] = [
|
||||||
|
{
|
||||||
|
path: "src/components/Button.tsx",
|
||||||
|
content: "export const Button = () => <button>Click</button>;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "src/utils/helpers.ts",
|
||||||
|
content: "export const add = (a: number, b: number) => a + b;",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const chatMessages: ModelMessage[] = [];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check fileIdToContent contains hashed content
|
||||||
|
const buttonHash = hashContent(files[0].content);
|
||||||
|
const helperHash = hashContent(files[1].content);
|
||||||
|
|
||||||
|
expect(result.fileIdToContent[buttonHash]).toBe(files[0].content);
|
||||||
|
expect(result.fileIdToContent[helperHash]).toBe(files[1].content);
|
||||||
|
|
||||||
|
// Check fileReferences
|
||||||
|
expect(result.fileReferences).toHaveLength(2);
|
||||||
|
expect(result.fileReferences[0]).toEqual({
|
||||||
|
path: "src/components/Button.tsx",
|
||||||
|
fileId: buttonHash,
|
||||||
|
});
|
||||||
|
expect(result.fileReferences[1]).toEqual({
|
||||||
|
path: "src/utils/helpers.ts",
|
||||||
|
fileId: helperHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
// messageIndexToFilePathToFileId should be empty
|
||||||
|
expect(result.messageIndexToFilePathToFileId).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty files array", async () => {
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.fileIdToContent).toEqual({});
|
||||||
|
expect(result.fileReferences).toEqual([]);
|
||||||
|
expect(result.messageIndexToFilePathToFileId).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("processing assistant messages", () => {
|
||||||
|
it("should process assistant messages with sourceCommitHash", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const fileContent = "const oldVersion = 'content';";
|
||||||
|
mockGetFileAtCommit.mockResolvedValue(fileContent);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
'I found this file: <dyad-read path="src/old.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify getFileAtCommit was called correctly
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledWith({
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/old.ts",
|
||||||
|
commitHash: "abc123",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check fileIdToContent
|
||||||
|
const fileHash = hashContent(fileContent);
|
||||||
|
expect(result.fileIdToContent[fileHash]).toBe(fileContent);
|
||||||
|
|
||||||
|
// Check messageIndexToFilePathToFileId
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({
|
||||||
|
"src/old.ts": fileHash,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process messages with array content type", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const fileContent = "const arrayContent = 'test';";
|
||||||
|
mockGetFileAtCommit.mockResolvedValue(fileContent);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: 'Here is the file: <dyad-read path="src/array.ts"></dyad-read>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Additional text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "def456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledWith({
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/array.ts",
|
||||||
|
commitHash: "def456",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileHash = hashContent(fileContent);
|
||||||
|
expect(result.fileIdToContent[fileHash]).toBe(fileContent);
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]["src/array.ts"]).toBe(
|
||||||
|
fileHash,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip user messages", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content:
|
||||||
|
'Check this: <dyad-read path="src/user-file.ts"></dyad-read>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// getFileAtCommit should not be called for user messages
|
||||||
|
expect(mockGetFileAtCommit).not.toHaveBeenCalled();
|
||||||
|
expect(result.messageIndexToFilePathToFileId).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip assistant messages without sourceCommitHash", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: 'File here: <dyad-read path="src/no-commit.ts"></dyad-read>',
|
||||||
|
// No providerOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
'Another file: <dyad-read path="src/no-commit2.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
// dyad-engine not set
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).not.toHaveBeenCalled();
|
||||||
|
expect(result.messageIndexToFilePathToFileId).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip messages with non-text content", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).not.toHaveBeenCalled();
|
||||||
|
expect(result.messageIndexToFilePathToFileId).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parsing multiple file paths", () => {
|
||||||
|
it("should process multiple files from dyad-code-search-result", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const file1Content = "file1 content";
|
||||||
|
const file2Content = "file2 content";
|
||||||
|
|
||||||
|
mockGetFileAtCommit
|
||||||
|
.mockResolvedValueOnce(file1Content)
|
||||||
|
.mockResolvedValueOnce(file2Content);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: `<dyad-code-search-result>
|
||||||
|
src/file1.ts
|
||||||
|
src/file2.ts
|
||||||
|
</dyad-code-search-result>`,
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledWith({
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/file1.ts",
|
||||||
|
commitHash: "commit1",
|
||||||
|
});
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledWith({
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/file2.ts",
|
||||||
|
commitHash: "commit1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const file1Hash = hashContent(file1Content);
|
||||||
|
const file2Hash = hashContent(file2Content);
|
||||||
|
|
||||||
|
expect(result.fileIdToContent[file1Hash]).toBe(file1Content);
|
||||||
|
expect(result.fileIdToContent[file2Hash]).toBe(file2Content);
|
||||||
|
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({
|
||||||
|
"src/file1.ts": file1Hash,
|
||||||
|
"src/file2.ts": file2Hash,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process mixed dyad-read and dyad-code-search-result tags", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
mockGetFileAtCommit
|
||||||
|
.mockResolvedValueOnce("file1")
|
||||||
|
.mockResolvedValueOnce("file2")
|
||||||
|
.mockResolvedValueOnce("file3");
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: `
|
||||||
|
<dyad-read path="src/file1.ts"></dyad-read>
|
||||||
|
|
||||||
|
<dyad-code-search-result>
|
||||||
|
src/file2.ts
|
||||||
|
src/file3.ts
|
||||||
|
</dyad-code-search-result>
|
||||||
|
`,
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "hash1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledTimes(3);
|
||||||
|
expect(Object.keys(result.messageIndexToFilePathToFileId[0])).toEqual([
|
||||||
|
"src/file1.ts",
|
||||||
|
"src/file2.ts",
|
||||||
|
"src/file3.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("error handling", () => {
|
||||||
|
it("should handle file not found (returns null)", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
// Simulate file not found
|
||||||
|
mockGetFileAtCommit.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
'Missing file: <dyad-read path="src/missing.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// File should not be in results
|
||||||
|
expect(result.fileIdToContent).toEqual({});
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle getFileAtCommit throwing an error", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
// Simulate error
|
||||||
|
mockGetFileAtCommit.mockRejectedValue(new Error("Git error"));
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: 'Error file: <dyad-read path="src/error.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
// Should not throw - errors are caught and logged
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalled();
|
||||||
|
expect(result.fileIdToContent).toEqual({});
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process some files successfully and skip others that error", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const successContent = "success file";
|
||||||
|
|
||||||
|
mockGetFileAtCommit
|
||||||
|
.mockResolvedValueOnce(successContent)
|
||||||
|
.mockRejectedValueOnce(new Error("Error"))
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: `<dyad-code-search-result>
|
||||||
|
src/success.ts
|
||||||
|
src/error.ts
|
||||||
|
src/missing.ts
|
||||||
|
</dyad-code-search-result>`,
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
// Only the successful file should be in results
|
||||||
|
const successHash = hashContent(successContent);
|
||||||
|
expect(result.fileIdToContent[successHash]).toBe(successContent);
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({
|
||||||
|
"src/success.ts": successHash,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("multiple messages", () => {
|
||||||
|
it("should process multiple messages with different commits", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const file1AtCommit1 = "file1 at commit1";
|
||||||
|
const file1AtCommit2 = "file1 at commit2 - different content";
|
||||||
|
|
||||||
|
mockGetFileAtCommit
|
||||||
|
.mockResolvedValueOnce(file1AtCommit1)
|
||||||
|
.mockResolvedValueOnce(file1AtCommit2);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Show me file1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: 'Here it is: <dyad-read path="src/file1.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Show me it again",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
'Here it is again: <dyad-read path="src/file1.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenNthCalledWith(1, {
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/file1.ts",
|
||||||
|
commitHash: "commit1",
|
||||||
|
});
|
||||||
|
expect(mockGetFileAtCommit).toHaveBeenNthCalledWith(2, {
|
||||||
|
path: appPath,
|
||||||
|
filePath: "src/file1.ts",
|
||||||
|
commitHash: "commit2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash1 = hashContent(file1AtCommit1);
|
||||||
|
const hash2 = hashContent(file1AtCommit2);
|
||||||
|
|
||||||
|
// Both versions should be in fileIdToContent
|
||||||
|
expect(result.fileIdToContent[hash1]).toBe(file1AtCommit1);
|
||||||
|
expect(result.fileIdToContent[hash2]).toBe(file1AtCommit2);
|
||||||
|
|
||||||
|
// Message index 1 (first assistant message)
|
||||||
|
expect(result.messageIndexToFilePathToFileId[1]).toEqual({
|
||||||
|
"src/file1.ts": hash1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message index 3 (second assistant message)
|
||||||
|
expect(result.messageIndexToFilePathToFileId[3]).toEqual({
|
||||||
|
"src/file1.ts": hash2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration with files parameter", () => {
|
||||||
|
it("should combine files parameter with versioned files from messages", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const versionedContent = "old version from git";
|
||||||
|
mockGetFileAtCommit.mockResolvedValue(versionedContent);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [
|
||||||
|
{
|
||||||
|
path: "src/current.ts",
|
||||||
|
content: "current version",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: 'Old version: <dyad-read path="src/old.ts"></dyad-read>',
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentHash = hashContent("current version");
|
||||||
|
const oldHash = hashContent(versionedContent);
|
||||||
|
|
||||||
|
// Both should be present
|
||||||
|
expect(result.fileIdToContent[currentHash]).toBe("current version");
|
||||||
|
expect(result.fileIdToContent[oldHash]).toBe(versionedContent);
|
||||||
|
|
||||||
|
// fileReferences should only include files from the files parameter
|
||||||
|
expect(result.fileReferences).toHaveLength(1);
|
||||||
|
expect(result.fileReferences[0].path).toBe("src/current.ts");
|
||||||
|
|
||||||
|
// messageIndexToFilePathToFileId should have the versioned file
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({
|
||||||
|
"src/old.ts": oldHash,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("content hashing", () => {
|
||||||
|
it("should deduplicate identical content with same hash", async () => {
|
||||||
|
const { getFileAtCommit } = await import("@/ipc/utils/git_utils");
|
||||||
|
const mockGetFileAtCommit = vi.mocked(getFileAtCommit);
|
||||||
|
|
||||||
|
const sameContent = "identical content";
|
||||||
|
|
||||||
|
// Both files have the same content
|
||||||
|
mockGetFileAtCommit
|
||||||
|
.mockResolvedValueOnce(sameContent)
|
||||||
|
.mockResolvedValueOnce(sameContent);
|
||||||
|
|
||||||
|
const files: CodebaseFile[] = [];
|
||||||
|
const chatMessages: ModelMessage[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: `<dyad-code-search-result>
|
||||||
|
src/file1.ts
|
||||||
|
src/file2.ts
|
||||||
|
</dyad-code-search-result>`,
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: "commit1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const appPath = "/test/app";
|
||||||
|
|
||||||
|
const result = await processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = hashContent(sameContent);
|
||||||
|
|
||||||
|
// fileIdToContent should only have one entry for the hash
|
||||||
|
expect(Object.keys(result.fileIdToContent)).toHaveLength(1);
|
||||||
|
expect(result.fileIdToContent[hash]).toBe(sameContent);
|
||||||
|
|
||||||
|
// Both files should point to the same hash
|
||||||
|
expect(result.messageIndexToFilePathToFileId[0]).toEqual({
|
||||||
|
"src/file1.ts": hash,
|
||||||
|
"src/file2.ts": hash,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,18 +32,16 @@ export function ProModeSelector() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSmartContextChange = (
|
const handleSmartContextChange = (newValue: "off" | "deep" | "balanced") => {
|
||||||
newValue: "off" | "conservative" | "balanced",
|
|
||||||
) => {
|
|
||||||
if (newValue === "off") {
|
if (newValue === "off") {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
enableProSmartFilesContextMode: false,
|
enableProSmartFilesContextMode: false,
|
||||||
proSmartContextOption: undefined,
|
proSmartContextOption: undefined,
|
||||||
});
|
});
|
||||||
} else if (newValue === "conservative") {
|
} else if (newValue === "deep") {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
enableProSmartFilesContextMode: true,
|
enableProSmartFilesContextMode: true,
|
||||||
proSmartContextOption: "conservative",
|
proSmartContextOption: "deep",
|
||||||
});
|
});
|
||||||
} else if (newValue === "balanced") {
|
} else if (newValue === "balanced") {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -90,16 +88,23 @@ export function ProModeSelector() {
|
|||||||
</div>
|
</div>
|
||||||
{!hasProKey && (
|
{!hasProKey && (
|
||||||
<div className="text-sm text-center text-muted-foreground">
|
<div className="text-sm text-center text-muted-foreground">
|
||||||
<a
|
<Tooltip>
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
<TooltipTrigger asChild>
|
||||||
onClick={() => {
|
<a
|
||||||
IpcClient.getInstance().openExternalUrl(
|
className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
||||||
"https://dyad.sh/pro#ai",
|
onClick={() => {
|
||||||
);
|
IpcClient.getInstance().openExternalUrl(
|
||||||
}}
|
"https://dyad.sh/pro#ai",
|
||||||
>
|
);
|
||||||
Unlock Pro modes
|
}}
|
||||||
</a>
|
>
|
||||||
|
Unlock Pro modes
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Visit dyad.sh/pro to unlock Pro features
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
@@ -239,33 +244,52 @@ function TurboEditsSelector({
|
|||||||
className="inline-flex rounded-md border border-input"
|
className="inline-flex rounded-md border border-input"
|
||||||
data-testid="turbo-edits-selector"
|
data-testid="turbo-edits-selector"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant={currentValue === "off" ? "default" : "ghost"}
|
<TooltipTrigger asChild>
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => onValueChange("off")}
|
variant={currentValue === "off" ? "default" : "ghost"}
|
||||||
disabled={!isTogglable}
|
size="sm"
|
||||||
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
onClick={() => onValueChange("off")}
|
||||||
>
|
disabled={!isTogglable}
|
||||||
Off
|
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
||||||
</Button>
|
>
|
||||||
<Button
|
Off
|
||||||
variant={currentValue === "v1" ? "default" : "ghost"}
|
</Button>
|
||||||
size="sm"
|
</TooltipTrigger>
|
||||||
onClick={() => onValueChange("v1")}
|
<TooltipContent>Disable Turbo Edits</TooltipContent>
|
||||||
disabled={!isTogglable}
|
</Tooltip>
|
||||||
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
Classic
|
<Button
|
||||||
</Button>
|
variant={currentValue === "v1" ? "default" : "ghost"}
|
||||||
<Button
|
size="sm"
|
||||||
variant={currentValue === "v2" ? "default" : "ghost"}
|
onClick={() => onValueChange("v1")}
|
||||||
size="sm"
|
disabled={!isTogglable}
|
||||||
onClick={() => onValueChange("v2")}
|
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
||||||
disabled={!isTogglable}
|
>
|
||||||
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
|
Classic
|
||||||
>
|
</Button>
|
||||||
Search & replace
|
</TooltipTrigger>
|
||||||
</Button>
|
<TooltipContent>
|
||||||
|
Uses a smaller model to complete edits
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentValue === "v2" ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onValueChange("v2")}
|
||||||
|
disabled={!isTogglable}
|
||||||
|
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
|
||||||
|
>
|
||||||
|
Search & replace
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Find and replaces specific text blocks
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -278,19 +302,19 @@ function SmartContextSelector({
|
|||||||
}: {
|
}: {
|
||||||
isTogglable: boolean;
|
isTogglable: boolean;
|
||||||
settings: UserSettings | null;
|
settings: UserSettings | null;
|
||||||
onValueChange: (value: "off" | "conservative" | "balanced") => void;
|
onValueChange: (value: "off" | "balanced" | "deep") => void;
|
||||||
}) {
|
}) {
|
||||||
// Determine current value based on settings
|
// Determine current value based on settings
|
||||||
const getCurrentValue = (): "off" | "conservative" | "balanced" => {
|
const getCurrentValue = (): "off" | "conservative" | "balanced" | "deep" => {
|
||||||
if (!settings?.enableProSmartFilesContextMode) {
|
if (!settings?.enableProSmartFilesContextMode) {
|
||||||
return "off";
|
return "off";
|
||||||
}
|
}
|
||||||
|
if (settings?.proSmartContextOption === "deep") {
|
||||||
|
return "deep";
|
||||||
|
}
|
||||||
if (settings?.proSmartContextOption === "balanced") {
|
if (settings?.proSmartContextOption === "balanced") {
|
||||||
return "balanced";
|
return "balanced";
|
||||||
}
|
}
|
||||||
if (settings?.proSmartContextOption === "conservative") {
|
|
||||||
return "conservative";
|
|
||||||
}
|
|
||||||
// Keep in sync with getModelClient in get_model_client.ts
|
// Keep in sync with getModelClient in get_model_client.ts
|
||||||
// If enabled but no option set (undefined/falsey), it's balanced
|
// If enabled but no option set (undefined/falsey), it's balanced
|
||||||
return "balanced";
|
return "balanced";
|
||||||
@@ -320,33 +344,53 @@ function SmartContextSelector({
|
|||||||
className="inline-flex rounded-md border border-input"
|
className="inline-flex rounded-md border border-input"
|
||||||
data-testid="smart-context-selector"
|
data-testid="smart-context-selector"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant={currentValue === "off" ? "default" : "ghost"}
|
<TooltipTrigger asChild>
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => onValueChange("off")}
|
variant={currentValue === "off" ? "default" : "ghost"}
|
||||||
disabled={!isTogglable}
|
size="sm"
|
||||||
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
onClick={() => onValueChange("off")}
|
||||||
>
|
disabled={!isTogglable}
|
||||||
Off
|
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
||||||
</Button>
|
>
|
||||||
<Button
|
Off
|
||||||
variant={currentValue === "conservative" ? "default" : "ghost"}
|
</Button>
|
||||||
size="sm"
|
</TooltipTrigger>
|
||||||
onClick={() => onValueChange("conservative")}
|
<TooltipContent>Disable Smart Context</TooltipContent>
|
||||||
disabled={!isTogglable}
|
</Tooltip>
|
||||||
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
Conservative
|
<Button
|
||||||
</Button>
|
variant={currentValue === "balanced" ? "default" : "ghost"}
|
||||||
<Button
|
size="sm"
|
||||||
variant={currentValue === "balanced" ? "default" : "ghost"}
|
onClick={() => onValueChange("balanced")}
|
||||||
size="sm"
|
disabled={!isTogglable}
|
||||||
onClick={() => onValueChange("balanced")}
|
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
|
||||||
disabled={!isTogglable}
|
>
|
||||||
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
|
Balanced
|
||||||
>
|
</Button>
|
||||||
Balanced
|
</TooltipTrigger>
|
||||||
</Button>
|
<TooltipContent>
|
||||||
|
Selects most relevant files with balanced context size
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentValue === "deep" ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onValueChange("deep")}
|
||||||
|
disabled={!isTogglable}
|
||||||
|
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
|
||||||
|
>
|
||||||
|
Deep
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<b>Experimental:</b> Keeps full conversation history for maximum
|
||||||
|
context and cache-optimized to control costs
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
31
src/components/chat/DyadCodeSearch.tsx
Normal file
31
src/components/chat/DyadCodeSearch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { FileCode } from "lucide-react";
|
||||||
|
|
||||||
|
interface DyadCodeSearchProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
node?: any;
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DyadCodeSearch: React.FC<DyadCodeSearchProps> = ({
|
||||||
|
children,
|
||||||
|
node: _node,
|
||||||
|
query: queryProp,
|
||||||
|
}) => {
|
||||||
|
const query = queryProp || (typeof children === "string" ? children : "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border my-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileCode size={16} className="text-purple-600" />
|
||||||
|
<div className="text-xs text-purple-600 font-medium">Code Search</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm italic text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
{query || children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
123
src/components/chat/DyadCodeSearchResult.tsx
Normal file
123
src/components/chat/DyadCodeSearchResult.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { ChevronDown, ChevronUp, FileCode, FileText } from "lucide-react";
|
||||||
|
|
||||||
|
interface DyadCodeSearchResultProps {
|
||||||
|
node?: any;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DyadCodeSearchResult: React.FC<DyadCodeSearchResultProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Parse file paths from children content
|
||||||
|
const files = useMemo(() => {
|
||||||
|
if (typeof children !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePaths: string[] = [];
|
||||||
|
const lines = children.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
// Skip empty lines and lines that look like tags
|
||||||
|
if (
|
||||||
|
trimmedLine &&
|
||||||
|
!trimmedLine.startsWith("<") &&
|
||||||
|
!trimmedLine.startsWith(">")
|
||||||
|
) {
|
||||||
|
filePaths.push(trimmedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePaths;
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border border-border my-2 cursor-pointer"
|
||||||
|
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-purple-600 bg-white dark:bg-zinc-900"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<FileCode size={16} className="text-purple-600" />
|
||||||
|
<span>Code Search Result</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File count when collapsed */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="absolute top-2 left-44 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">
|
||||||
|
Found {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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,6 +23,8 @@ import { DyadMcpToolResult } from "./DyadMcpToolResult";
|
|||||||
import { DyadWebSearchResult } from "./DyadWebSearchResult";
|
import { DyadWebSearchResult } from "./DyadWebSearchResult";
|
||||||
import { DyadWebSearch } from "./DyadWebSearch";
|
import { DyadWebSearch } from "./DyadWebSearch";
|
||||||
import { DyadWebCrawl } from "./DyadWebCrawl";
|
import { DyadWebCrawl } from "./DyadWebCrawl";
|
||||||
|
import { DyadCodeSearchResult } from "./DyadCodeSearchResult";
|
||||||
|
import { DyadCodeSearch } from "./DyadCodeSearch";
|
||||||
import { DyadRead } from "./DyadRead";
|
import { DyadRead } from "./DyadRead";
|
||||||
import { mapActionToButton } from "./ChatInput";
|
import { mapActionToButton } from "./ChatInput";
|
||||||
import { SuggestedAction } from "@/lib/schemas";
|
import { SuggestedAction } from "@/lib/schemas";
|
||||||
@@ -210,6 +212,8 @@ function parseCustomTags(content: string): ContentPiece[] {
|
|||||||
"dyad-web-search-result",
|
"dyad-web-search-result",
|
||||||
"dyad-web-search",
|
"dyad-web-search",
|
||||||
"dyad-web-crawl",
|
"dyad-web-crawl",
|
||||||
|
"dyad-code-search-result",
|
||||||
|
"dyad-code-search",
|
||||||
"dyad-read",
|
"dyad-read",
|
||||||
"think",
|
"think",
|
||||||
"dyad-command",
|
"dyad-command",
|
||||||
@@ -332,6 +336,26 @@ function renderCustomTag(
|
|||||||
{content}
|
{content}
|
||||||
</DyadWebCrawl>
|
</DyadWebCrawl>
|
||||||
);
|
);
|
||||||
|
case "dyad-code-search":
|
||||||
|
return (
|
||||||
|
<DyadCodeSearch
|
||||||
|
node={{
|
||||||
|
properties: {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadCodeSearch>
|
||||||
|
);
|
||||||
|
case "dyad-code-search-result":
|
||||||
|
return (
|
||||||
|
<DyadCodeSearchResult
|
||||||
|
node={{
|
||||||
|
properties: {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadCodeSearchResult>
|
||||||
|
);
|
||||||
case "dyad-web-search-result":
|
case "dyad-web-search-result":
|
||||||
return (
|
return (
|
||||||
<DyadWebSearchResult
|
<DyadWebSearchResult
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export const messages = sqliteTable("messages", {
|
|||||||
approvalState: text("approval_state", {
|
approvalState: text("approval_state", {
|
||||||
enum: ["approved", "rejected"],
|
enum: ["approved", "rejected"],
|
||||||
}),
|
}),
|
||||||
|
// The commit hash of the codebase at the time the message was created
|
||||||
|
sourceCommitHash: text("source_commit_hash"),
|
||||||
|
// The commit hash of the codebase at the time the message was sent
|
||||||
commitHash: text("commit_hash"),
|
commitHash: text("commit_hash"),
|
||||||
requestId: text("request_id"),
|
requestId: text("request_id"),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ import { mcpManager } from "../utils/mcp_manager";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { isTurboEditsV2Enabled } from "@/lib/schemas";
|
import { isTurboEditsV2Enabled } from "@/lib/schemas";
|
||||||
import { AI_STREAMING_ERROR_MESSAGE_PREFIX } from "@/shared/texts";
|
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>;
|
type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
|
||||||
|
|
||||||
@@ -407,6 +412,9 @@ ${componentSnippet}
|
|||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "", // Start with empty content
|
content: "", // Start with empty content
|
||||||
requestId: dyadRequestId,
|
requestId: dyadRequestId,
|
||||||
|
sourceCommitHash: await getCurrentCommitHash({
|
||||||
|
path: getDyadAppPath(chat.app.path),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -523,12 +531,20 @@ ${componentSnippet}
|
|||||||
const messageHistory = updatedChat.messages.map((message) => ({
|
const messageHistory = updatedChat.messages.map((message) => ({
|
||||||
role: message.role as "user" | "assistant" | "system",
|
role: message.role as "user" | "assistant" | "system",
|
||||||
content: message.content,
|
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
|
// Limit chat history based on maxChatTurnsInContext setting
|
||||||
// We add 1 because the current prompt counts as a turn.
|
// We add 1 because the current prompt counts as a turn.
|
||||||
const maxChatTurns =
|
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
|
// If we need to limit the context, we take only the most recent turns
|
||||||
let limitedMessageHistory = messageHistory;
|
let limitedMessageHistory = messageHistory;
|
||||||
@@ -713,6 +729,11 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
settings.selectedChatMode === "ask"
|
settings.selectedChatMode === "ask"
|
||||||
? removeDyadTags(removeNonEssentialTags(msg.content))
|
? removeDyadTags(removeNonEssentialTags(msg.content))
|
||||||
: removeNonEssentialTags(msg.content),
|
: removeNonEssentialTags(msg.content),
|
||||||
|
providerOptions: {
|
||||||
|
"dyad-engine": {
|
||||||
|
sourceCommitHash: msg.sourceCommitHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let chatMessages: ModelMessage[] = [
|
let chatMessages: ModelMessage[] = [
|
||||||
@@ -776,12 +797,22 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
} else {
|
} else {
|
||||||
logger.log("sending AI request");
|
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
|
// Build provider options with correct Google/Vertex thinking config gating
|
||||||
const providerOptions: Record<string, any> = {
|
const providerOptions: Record<string, any> = {
|
||||||
"dyad-engine": {
|
"dyad-engine": {
|
||||||
|
dyadAppId: updatedChat.app.id,
|
||||||
dyadRequestId,
|
dyadRequestId,
|
||||||
dyadDisableFiles,
|
dyadDisableFiles,
|
||||||
dyadFiles: files,
|
dyadFiles: versionedFiles ? undefined : files,
|
||||||
|
dyadVersionedFiles: versionedFiles,
|
||||||
dyadMentionedApps: mentionedAppsCodebases.map(
|
dyadMentionedApps: mentionedAppsCodebases.map(
|
||||||
({ files, appName }) => ({
|
({ files, appName }) => ({
|
||||||
appName,
|
appName,
|
||||||
@@ -979,21 +1010,48 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
settings.selectedChatMode !== "ask" &&
|
settings.selectedChatMode !== "ask" &&
|
||||||
isTurboEditsV2Enabled(settings)
|
isTurboEditsV2Enabled(settings)
|
||||||
) {
|
) {
|
||||||
const issues = await dryRunSearchReplace({
|
let issues = await dryRunSearchReplace({
|
||||||
fullResponse,
|
fullResponse,
|
||||||
appPath: getDyadAppPath(updatedChat.app.path),
|
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(
|
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
|
const formattedSearchReplaceIssues = issues
|
||||||
.map(({ filePath, error }) => {
|
.map(({ filePath, error }) => {
|
||||||
return `File path: ${filePath}\nError: ${error}`;
|
return `File path: ${filePath}\nError: ${error}`;
|
||||||
})
|
})
|
||||||
.join("\n\n");
|
.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>`;
|
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 } =
|
const { fullStream: fixSearchReplaceStream } =
|
||||||
await simpleStreamText({
|
await simpleStreamText({
|
||||||
@@ -1001,16 +1059,13 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
chatMessages: [
|
chatMessages: [
|
||||||
...chatMessages,
|
...chatMessages,
|
||||||
{ role: "assistant", content: originalFullResponse },
|
{ role: "assistant", content: originalFullResponse },
|
||||||
{
|
...previousAttempts,
|
||||||
role: "user",
|
userPrompt,
|
||||||
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}`,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
modelClient,
|
modelClient,
|
||||||
files: files,
|
files: files,
|
||||||
});
|
});
|
||||||
|
previousAttempts.push(userPrompt);
|
||||||
const result = await processStreamChunks({
|
const result = await processStreamChunks({
|
||||||
fullStream: fixSearchReplaceStream,
|
fullStream: fixSearchReplaceStream,
|
||||||
fullResponse,
|
fullResponse,
|
||||||
@@ -1019,6 +1074,16 @@ ${formattedSearchReplaceIssues}`,
|
|||||||
processResponseChunkUpdate,
|
processResponseChunkUpdate,
|
||||||
});
|
});
|
||||||
fullResponse = result.fullResponse;
|
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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ export async function processFullResponseActions(
|
|||||||
const original = await readFile(fullFilePath, "utf8");
|
const original = await readFile(fullFilePath, "utf8");
|
||||||
const result = applySearchReplace(original, tag.content);
|
const result = applySearchReplace(original, tag.content);
|
||||||
if (!result.success || typeof result.content !== "string") {
|
if (!result.success || typeof result.content !== "string") {
|
||||||
// Do not show warning to user because we already attempt to do a <dyad-write> tag to fix it.
|
// Do not show warning to user because we already attempt to do a <dyad-write> and/or a subsequent <dyad-search-replace> tag to fix it.
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Failed to apply search-replace to ${filePath}: ${result.error ?? "unknown"}`,
|
`Failed to apply search-replace to ${filePath}: ${result.error ?? "unknown"}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import pathModule from "node:path";
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { readSettings } from "../../main/settings";
|
import { readSettings } from "../../main/settings";
|
||||||
|
import log from "electron-log";
|
||||||
|
const logger = log.scope("git_utils");
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
async function verboseExecAsync(
|
async function verboseExecAsync(
|
||||||
@@ -26,6 +27,18 @@ async function verboseExecAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurrentCommitHash({
|
||||||
|
path,
|
||||||
|
}: {
|
||||||
|
path: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
return await git.resolveRef({
|
||||||
|
fs,
|
||||||
|
dir: path,
|
||||||
|
ref: "HEAD",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function gitCommit({
|
export async function gitCommit({
|
||||||
path,
|
path,
|
||||||
message,
|
message,
|
||||||
@@ -166,3 +179,45 @@ export async function gitAddAll({ path }: { path: string }): Promise<void> {
|
|||||||
return git.add({ fs, dir: path, filepath: "." });
|
return git.add({ fs, dir: path, filepath: "." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFileAtCommit({
|
||||||
|
path,
|
||||||
|
filePath,
|
||||||
|
commitHash,
|
||||||
|
}: {
|
||||||
|
path: string;
|
||||||
|
filePath: string;
|
||||||
|
commitHash: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const settings = readSettings();
|
||||||
|
if (settings.enableNativeGit) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
`git -C "${path}" show "${commitHash}:${filePath}"`,
|
||||||
|
);
|
||||||
|
return stdout;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(
|
||||||
|
`Error getting file at commit ${commitHash}: ${error.message}`,
|
||||||
|
);
|
||||||
|
// File doesn't exist at this commit
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const { blob } = await git.readBlob({
|
||||||
|
fs,
|
||||||
|
dir: path,
|
||||||
|
oid: commitHash,
|
||||||
|
filepath: filePath,
|
||||||
|
});
|
||||||
|
return Buffer.from(blob).toString("utf-8");
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(
|
||||||
|
`Error getting file at commit ${commitHash}: ${error.message}`,
|
||||||
|
);
|
||||||
|
// File doesn't exist at this commit
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ or to provide a custom fetch implementation for e.g. testing.
|
|||||||
enableLazyEdits?: boolean;
|
enableLazyEdits?: boolean;
|
||||||
enableSmartFilesContext?: boolean;
|
enableSmartFilesContext?: boolean;
|
||||||
enableWebSearch?: boolean;
|
enableWebSearch?: boolean;
|
||||||
smartContextMode?: "balanced" | "conservative";
|
smartContextMode?: "balanced" | "conservative" | "deep";
|
||||||
};
|
};
|
||||||
settings: UserSettings;
|
settings: UserSettings;
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,10 @@ export function createDyadEngine(
|
|||||||
options.settings,
|
options.settings,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
const dyadVersionedFiles = parsedBody.dyadVersionedFiles;
|
||||||
|
if ("dyadVersionedFiles" in parsedBody) {
|
||||||
|
delete parsedBody.dyadVersionedFiles;
|
||||||
|
}
|
||||||
const dyadFiles = parsedBody.dyadFiles;
|
const dyadFiles = parsedBody.dyadFiles;
|
||||||
if ("dyadFiles" in parsedBody) {
|
if ("dyadFiles" in parsedBody) {
|
||||||
delete parsedBody.dyadFiles;
|
delete parsedBody.dyadFiles;
|
||||||
@@ -133,6 +137,10 @@ export function createDyadEngine(
|
|||||||
if ("dyadRequestId" in parsedBody) {
|
if ("dyadRequestId" in parsedBody) {
|
||||||
delete parsedBody.dyadRequestId;
|
delete parsedBody.dyadRequestId;
|
||||||
}
|
}
|
||||||
|
const dyadAppId = parsedBody.dyadAppId;
|
||||||
|
if ("dyadAppId" in parsedBody) {
|
||||||
|
delete parsedBody.dyadAppId;
|
||||||
|
}
|
||||||
const dyadDisableFiles = parsedBody.dyadDisableFiles;
|
const dyadDisableFiles = parsedBody.dyadDisableFiles;
|
||||||
if ("dyadDisableFiles" in parsedBody) {
|
if ("dyadDisableFiles" in parsedBody) {
|
||||||
delete parsedBody.dyadDisableFiles;
|
delete parsedBody.dyadDisableFiles;
|
||||||
@@ -151,14 +159,16 @@ export function createDyadEngine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add files to the request if they exist
|
// Add files to the request if they exist
|
||||||
if (dyadFiles?.length && !dyadDisableFiles) {
|
if (!dyadDisableFiles) {
|
||||||
parsedBody.dyad_options = {
|
parsedBody.dyad_options = {
|
||||||
files: dyadFiles,
|
files: dyadFiles,
|
||||||
|
versioned_files: dyadVersionedFiles,
|
||||||
enable_lazy_edits: options.dyadOptions.enableLazyEdits,
|
enable_lazy_edits: options.dyadOptions.enableLazyEdits,
|
||||||
enable_smart_files_context:
|
enable_smart_files_context:
|
||||||
options.dyadOptions.enableSmartFilesContext,
|
options.dyadOptions.enableSmartFilesContext,
|
||||||
smart_context_mode: options.dyadOptions.smartContextMode,
|
smart_context_mode: options.dyadOptions.smartContextMode,
|
||||||
enable_web_search: options.dyadOptions.enableWebSearch,
|
enable_web_search: options.dyadOptions.enableWebSearch,
|
||||||
|
app_id: dyadAppId,
|
||||||
};
|
};
|
||||||
if (dyadMentionedApps?.length) {
|
if (dyadMentionedApps?.length) {
|
||||||
parsedBody.dyad_options.mentioned_apps = dyadMentionedApps;
|
parsedBody.dyad_options.mentioned_apps = dyadMentionedApps;
|
||||||
|
|||||||
219
src/ipc/utils/versioned_codebase_context.ts
Normal file
219
src/ipc/utils/versioned_codebase_context.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { CodebaseFile, CodebaseFileReference } from "@/utils/codebase";
|
||||||
|
import { ModelMessage } from "@ai-sdk/provider-utils";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import log from "electron-log";
|
||||||
|
import { getFileAtCommit } from "./git_utils";
|
||||||
|
import { normalizePath } from "../../../shared/normalizePath";
|
||||||
|
|
||||||
|
const logger = log.scope("versioned_codebase_context");
|
||||||
|
|
||||||
|
export interface VersionedFiles {
|
||||||
|
fileIdToContent: Record<string, string>;
|
||||||
|
fileReferences: CodebaseFileReference[];
|
||||||
|
messageIndexToFilePathToFileId: Record<number, Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DyadEngineProviderOptions {
|
||||||
|
sourceCommitHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse file paths from assistant message content.
|
||||||
|
* Extracts files from <dyad-read> and <dyad-code-search-result> tags.
|
||||||
|
*/
|
||||||
|
export function parseFilesFromMessage(content: string): string[] {
|
||||||
|
const filePaths: string[] = [];
|
||||||
|
const seenPaths = new Set<string>();
|
||||||
|
|
||||||
|
// Create an array of matches with their positions to maintain order
|
||||||
|
interface TagMatch {
|
||||||
|
index: number;
|
||||||
|
filePaths: string[];
|
||||||
|
}
|
||||||
|
const matches: TagMatch[] = [];
|
||||||
|
|
||||||
|
// Parse <dyad-read path="$filePath"></dyad-read>
|
||||||
|
const dyadReadRegex = /<dyad-read\s+path="([^"]+)"\s*><\/dyad-read>/gs;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = dyadReadRegex.exec(content)) !== null) {
|
||||||
|
const filePath = normalizePath(match[1].trim());
|
||||||
|
if (filePath) {
|
||||||
|
matches.push({
|
||||||
|
index: match.index,
|
||||||
|
filePaths: [filePath],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse <dyad-code-search-result>...</dyad-code-search-result>
|
||||||
|
const codeSearchRegex =
|
||||||
|
/<dyad-code-search-result>(.*?)<\/dyad-code-search-result>/gs;
|
||||||
|
while ((match = codeSearchRegex.exec(content)) !== null) {
|
||||||
|
const innerContent = match[1];
|
||||||
|
const paths: string[] = [];
|
||||||
|
// Split by newlines and extract each file path
|
||||||
|
const lines = innerContent.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (
|
||||||
|
trimmedLine &&
|
||||||
|
!trimmedLine.startsWith("<") &&
|
||||||
|
!trimmedLine.startsWith(">")
|
||||||
|
) {
|
||||||
|
paths.push(normalizePath(trimmedLine));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paths.length > 0) {
|
||||||
|
matches.push({
|
||||||
|
index: match.index,
|
||||||
|
filePaths: paths,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort matches by their position in the original content
|
||||||
|
matches.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
// Add file paths in order, deduplicating as we go
|
||||||
|
for (const match of matches) {
|
||||||
|
for (const path of match.filePaths) {
|
||||||
|
if (!seenPaths.has(path)) {
|
||||||
|
seenPaths.add(path);
|
||||||
|
filePaths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processChatMessagesWithVersionedFiles({
|
||||||
|
files,
|
||||||
|
chatMessages,
|
||||||
|
appPath,
|
||||||
|
}: {
|
||||||
|
files: CodebaseFile[];
|
||||||
|
chatMessages: ModelMessage[];
|
||||||
|
appPath: string;
|
||||||
|
}): Promise<VersionedFiles> {
|
||||||
|
const fileIdToContent: Record<string, string> = {};
|
||||||
|
const fileReferences: CodebaseFileReference[] = [];
|
||||||
|
const messageIndexToFilePathToFileId: Record<
|
||||||
|
number,
|
||||||
|
Record<string, string>
|
||||||
|
> = {};
|
||||||
|
for (const file of files) {
|
||||||
|
// Generate SHA-256 hash of content as fileId
|
||||||
|
const fileId = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(file.content)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
fileIdToContent[fileId] = file.content;
|
||||||
|
const { content: _content, ...restOfFile } = file;
|
||||||
|
|
||||||
|
fileReferences.push({
|
||||||
|
...restOfFile,
|
||||||
|
fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let messageIndex = 0;
|
||||||
|
messageIndex < chatMessages.length;
|
||||||
|
messageIndex++
|
||||||
|
) {
|
||||||
|
const message = chatMessages[messageIndex];
|
||||||
|
|
||||||
|
// Only process assistant messages
|
||||||
|
if (message.role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract sourceCommitHash from providerOptions
|
||||||
|
const engineOptions = message.providerOptions?.[
|
||||||
|
"dyad-engine"
|
||||||
|
] as unknown as DyadEngineProviderOptions;
|
||||||
|
const sourceCommitHash = engineOptions?.sourceCommitHash;
|
||||||
|
|
||||||
|
// Skip messages without sourceCommitHash
|
||||||
|
if (!sourceCommitHash) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get message content as text
|
||||||
|
const content = message.content;
|
||||||
|
let textContent: string;
|
||||||
|
|
||||||
|
if (typeof content !== "string") {
|
||||||
|
// Handle array of parts (text, images, etc.)
|
||||||
|
textContent = content
|
||||||
|
.filter((part) => part.type === "text")
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
if (!textContent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Message content is already a string
|
||||||
|
textContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse file paths from message content
|
||||||
|
const filePaths = parseFilesFromMessage(textContent);
|
||||||
|
const filePathsToFileIds: Record<string, string> = {};
|
||||||
|
messageIndexToFilePathToFileId[messageIndex] = filePathsToFileIds;
|
||||||
|
|
||||||
|
// Parallelize file content fetching
|
||||||
|
const fileContentPromises = filePaths.map((filePath) =>
|
||||||
|
getFileAtCommit({
|
||||||
|
path: appPath,
|
||||||
|
filePath,
|
||||||
|
commitHash: sourceCommitHash,
|
||||||
|
}).then(
|
||||||
|
(content) => ({ filePath, content, status: "fulfilled" as const }),
|
||||||
|
(error) => ({ filePath, error, status: "rejected" as const }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(fileContentPromises);
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
logger.error(
|
||||||
|
`Error reading file ${result.filePath} at commit ${sourceCommitHash}:`,
|
||||||
|
result.error,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filePath, content: fileContent } = result;
|
||||||
|
|
||||||
|
if (fileContent === null) {
|
||||||
|
logger.warn(
|
||||||
|
`File ${filePath} not found at commit ${sourceCommitHash} for message ${messageIndex}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate SHA-256 hash of content as fileId
|
||||||
|
const fileId = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(fileContent)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
// Store in fileIdToContent
|
||||||
|
fileIdToContent[fileId] = fileContent;
|
||||||
|
|
||||||
|
// Add to this message's file IDs
|
||||||
|
filePathsToFileIds[filePath] = fileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileIdToContent,
|
||||||
|
fileReferences,
|
||||||
|
messageIndexToFilePathToFileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -235,7 +235,9 @@ export const UserSettingsSchema = z.object({
|
|||||||
proLazyEditsMode: z.enum(["off", "v1", "v2"]).optional(),
|
proLazyEditsMode: z.enum(["off", "v1", "v2"]).optional(),
|
||||||
enableProSmartFilesContextMode: z.boolean().optional(),
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
||||||
enableProWebSearch: z.boolean().optional(),
|
enableProWebSearch: z.boolean().optional(),
|
||||||
proSmartContextOption: z.enum(["balanced", "conservative"]).optional(),
|
proSmartContextOption: z
|
||||||
|
.enum(["balanced", "conservative", "deep"])
|
||||||
|
.optional(),
|
||||||
selectedTemplateId: z.string(),
|
selectedTemplateId: z.string(),
|
||||||
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
||||||
selectedChatMode: ChatModeSchema.optional(),
|
selectedChatMode: ChatModeSchema.optional(),
|
||||||
|
|||||||
@@ -134,7 +134,10 @@ export function readSettings(): UserSettings {
|
|||||||
|
|
||||||
// Validate and merge with defaults
|
// Validate and merge with defaults
|
||||||
const validatedSettings = UserSettingsSchema.parse(combinedSettings);
|
const validatedSettings = UserSettingsSchema.parse(combinedSettings);
|
||||||
|
// "conservative" is deprecated, use undefined to use the default value
|
||||||
|
if (validatedSettings.proSmartContextOption === "conservative") {
|
||||||
|
validatedSettings.proSmartContextOption = undefined;
|
||||||
|
}
|
||||||
return validatedSettings;
|
return validatedSettings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error reading settings:", error);
|
logger.error("Error reading settings:", error);
|
||||||
|
|||||||
@@ -411,12 +411,19 @@ ${content}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CodebaseFile = {
|
export interface BaseFile {
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface CodebaseFile extends BaseFile {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodebaseFileReference extends BaseFile {
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract and format codebase files as a string to be included in prompts
|
* Extract and format codebase files as a string to be included in prompts
|
||||||
|
|||||||
@@ -131,17 +131,36 @@ export default Index;
|
|||||||
lastMessage &&
|
lastMessage &&
|
||||||
typeof lastMessage.content === "string" &&
|
typeof lastMessage.content === "string" &&
|
||||||
lastMessage.content.startsWith(
|
lastMessage.content.startsWith(
|
||||||
"There was an issue with the following `dyad-search-replace` tags",
|
"There was an issue with the following `dyad-search-replace` tags.",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Fix errors in create-ts-errors.md and introduce a new error
|
if (lastMessage.content.includes("Make sure you use `dyad-read`")) {
|
||||||
messageContent =
|
// Fix errors in create-ts-errors.md and introduce a new error
|
||||||
`
|
messageContent =
|
||||||
|
`
|
||||||
|
<dyad-read path="src/pages/Index.tsx"></dyad-read>
|
||||||
|
|
||||||
|
<dyad-search-replace path="src/pages/Index.tsx">
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// STILL Intentionally DO NOT MATCH ANYTHING TO TRIGGER FALLBACK
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||||
|
=======
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Welcome to the UPDATED App</h1>
|
||||||
|
>>>>>>> REPLACE
|
||||||
|
</dyad-search-replace>
|
||||||
|
` +
|
||||||
|
"\n\n" +
|
||||||
|
generateDump(req);
|
||||||
|
} else {
|
||||||
|
// Fix errors in create-ts-errors.md and introduce a new error
|
||||||
|
messageContent =
|
||||||
|
`
|
||||||
<dyad-write path="src/pages/Index.tsx" description="Rewrite file.">
|
<dyad-write path="src/pages/Index.tsx" description="Rewrite file.">
|
||||||
// FILE IS REPLACED WITH FALLBACK WRITE.
|
// FILE IS REPLACED WITH FALLBACK WRITE.
|
||||||
</dyad-write>` +
|
</dyad-write>` +
|
||||||
"\n\n" +
|
"\n\n" +
|
||||||
generateDump(req);
|
generateDump(req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("LASTMESSAGE", lastMessage);
|
console.error("LASTMESSAGE", lastMessage);
|
||||||
|
|||||||
Reference in New Issue
Block a user