Unverified 提交 534cbad9 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Allow manual context management (#376)

上级 e7941bc6
ALTER TABLE `apps` ADD `chat_context` text;
\ No newline at end of file
{
"version": "6",
"dialect": "sqlite",
"id": "164b6b9d-8df1-41f0-b3d2-5fe479312bdc",
"prevId": "0a47ec41-9477-4457-b3e8-e5ecb3e3a855",
"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
},
"supabase_project_id": {
"name": "supabase_project_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"chat_context": {
"name": "chat_context",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"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": {}
},
"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
},
"commit_hash": {
"name": "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": {
"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": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
\ No newline at end of file
......@@ -43,6 +43,13 @@
"when": 1747095436506,
"tag": "0005_clumsy_namor",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1749515724373,
"tag": "0006_mushy_squirrel_girl",
"breakpoints": true
}
]
}
\ No newline at end of file
import { test } from "./helpers/test_helper";
test("manage context - default", async ({ po }) => {
await po.setUp();
await po.importApp("context-manage");
const dialog = await po.openContextFilesPicker();
await po.snapshotDialog();
await dialog.addManualContextFile("DELETETHIS");
await dialog.removeManualContextFile();
await dialog.addManualContextFile("src/**/*.ts");
await dialog.addManualContextFile("src/sub/**");
await po.snapshotDialog();
await dialog.close();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("all-messages");
});
test("manage context - smart context", async ({ po }) => {
await po.setUpDyadPro();
await po.selectModel({ provider: "Google", model: "Gemini 2.5 Pro" });
await po.importApp("context-manage");
let dialog = await po.openContextFilesPicker();
await po.snapshotDialog();
await dialog.addManualContextFile("src/**/*.ts");
await dialog.addManualContextFile("src/sub/**");
await dialog.addAutoIncludeContextFile("a.ts");
await dialog.addAutoIncludeContextFile("manual/**");
await po.snapshotDialog();
await dialog.close();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
await po.snapshotServerDump("all-messages");
// Disabling smart context will automatically disable
// the auto-includes.
const proModesDialog = await po.openProModesDialog();
await proModesDialog.toggleSmartContext();
await proModesDialog.close();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
// Removing manual context files will result in all files being included.
dialog = await po.openContextFilesPicker();
await dialog.removeManualContextFile();
await dialog.removeManualContextFile();
await dialog.close();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
});
test("manage context - smart context - auto-includes only", async ({ po }) => {
await po.setUpDyadPro();
await po.selectModel({ provider: "Google", model: "Gemini 2.5 Pro" });
await po.importApp("context-manage");
const dialog = await po.openContextFilesPicker();
await po.snapshotDialog();
await dialog.addAutoIncludeContextFile("a.ts");
await dialog.addAutoIncludeContextFile("manual/**");
await po.snapshotDialog();
await dialog.close();
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
});
# THIS FILE SHOULD NOT BE SENT IN THE CONTEXT
\ No newline at end of file
// exclude.ts: this file is not in any of the globs
// exclude.tsx: this file is not in any of the globs
// very-large-file.ts
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
// 1234567890
import { test as base, Page, expect } from "@playwright/test";
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
import * as eph from "electron-playwright-helpers";
import { ElectronApplication, _electron as electron } from "playwright";
import fs from "fs";
import path from "path";
......@@ -16,6 +16,54 @@ export const Timeout = {
MEDIUM: process.env.CI ? 30_000 : 15_000,
};
export class ContextFilesPickerDialog {
constructor(
public page: Page,
public close: () => Promise<void>,
) {}
async addManualContextFile(path: string) {
await this.page.getByTestId("manual-context-files-input").fill(path);
await this.page.getByTestId("manual-context-files-add-button").click();
}
async addAutoIncludeContextFile(path: string) {
await this.page.getByTestId("auto-include-context-files-input").fill(path);
await this.page
.getByTestId("auto-include-context-files-add-button")
.click();
}
async removeManualContextFile() {
await this.page
.getByTestId("manual-context-files-remove-button")
.first()
.click();
}
async removeAutoIncludeContextFile() {
await this.page
.getByTestId("auto-include-context-files-remove-button")
.first()
.click();
}
}
class ProModesDialog {
constructor(
public page: Page,
public close: () => Promise<void>,
) {}
async toggleSmartContext() {
await this.page.getByRole("switch", { name: "Smart Context" }).click();
}
async toggleTurboEdits() {
await this.page.getByRole("switch", { name: "Turbo Edits" }).click();
}
}
export class PageObject {
private userDataDir: string;
......@@ -45,6 +93,15 @@ export class PageObject {
await this.selectTestModel();
}
async importApp(appDir: string) {
await this.page.getByRole("button", { name: "Import App" }).click();
await eph.stubDialog(this.electronApp, "showOpenDialog", {
filePaths: [path.join(__dirname, "..", "fixtures", "import-app", appDir)],
});
await this.page.getByRole("button", { name: "Select Folder" }).click();
await this.page.getByRole("button", { name: "Import" }).click();
}
async setUpDyadPro({ autoApprove = false }: { autoApprove?: boolean } = {}) {
await this.goToSettingsTab();
if (autoApprove) {
......@@ -74,6 +131,32 @@ export class PageObject {
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
}
async openContextFilesPicker() {
const contextButton = this.page.getByRole("button", {
name: "Context",
exact: true,
});
await contextButton.click();
return new ContextFilesPickerDialog(this.page, async () => {
await contextButton.click();
});
}
async openProModesDialog(): Promise<ProModesDialog> {
const proButton = this.page
// Assumes you're on the chat page.
.getByTestId("chat-input-container")
.getByRole("button", { name: "Pro", exact: true });
await proButton.click();
return new ProModesDialog(this.page, async () => {
await proButton.click();
});
}
async snapshotDialog() {
await expect(this.page.getByRole("dialog")).toMatchAriaSnapshot();
}
async snapshotAppFiles({ name }: { name?: string } = {}) {
const appPath = await this.getCurrentAppPath();
if (!appPath || !fs.existsSync(appPath)) {
......@@ -214,7 +297,17 @@ export class PageObject {
// Perform snapshot comparison
const parsedDump = JSON.parse(dumpContent);
if (type === "request") {
expect(dumpContent).toMatchSnapshot(name);
parsedDump["body"]["messages"] = parsedDump["body"]["messages"].map(
(message: any) => {
if (message.role === "system") {
message.content = "[[SYSTEM_MESSAGE]]";
}
return message;
},
);
expect(
JSON.stringify(parsedDump, null, 2).replace(/\\r\\n/g, "\\n"),
).toMatchSnapshot(name);
return;
}
expect(
......@@ -555,9 +648,9 @@ export const test = base.extend<{
electronApp: [
async ({}, use) => {
// find the latest build in the out directory
const latestBuild = findLatestBuild();
const latestBuild = eph.findLatestBuild();
// parse the directory and find paths and other info
const appInfo = parseElectronApp(latestBuild);
const appInfo = eph.parseElectronApp(latestBuild);
process.env.OLLAMA_HOST = "http://localhost:3500/ollama";
process.env.LM_STUDIO_BASE_URL_FOR_TESTING =
"http://localhost:3500/lmstudio";
......
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use the entire codebase as context.
\ No newline at end of file
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button:
- img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/
- button:
- img
\ No newline at end of file
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- textbox "src/**/*.config.ts"
- button "Add"
\ No newline at end of file
{
"body": {
"model": "gemini/gemini-2.5-pro-preview-05-06",
"max_tokens": 65535,
"temperature": 0,
"messages": [
{
"role": "system",
"content": "[[SYSTEM_MESSAGE]]"
},
{
"role": "user",
"content": "[dump]"
}
],
"stream": true,
"dyad_options": {
"files": [
{
"path": "a.ts",
"content": "// a.ts\n",
"force": true
},
{
"path": "AI_RULES.md",
"content": "# AI_RULES.md\n",
"force": false
},
{
"path": "exclude/exclude.ts",
"content": "// exclude.ts: this file is not in any of the globs\n",
"force": false
},
{
"path": "exclude/exclude.tsx",
"content": "// exclude.tsx: this file is not in any of the globs\n",
"force": false
},
{
"path": "manual/file.ts",
"content": "",
"force": true
},
{
"path": "manual/sub-manual/sub-manual.js",
"content": "",
"force": true
},
{
"path": "src/components/ui/button.tsx",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/components/ui/helper.ts",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/dir/some.css",
"content": "/* some.css */\n",
"force": false
},
{
"path": "src/foo.ts",
"content": "// foo.ts\n",
"force": false
},
{
"path": "src/sub/sub1.ts",
"content": "// sub/sub1.ts\n",
"force": false
},
{
"path": "src/sub/sub2.tsx",
"content": "// sub/sub2.tsx\n",
"force": false
},
{
"path": "src/very-large-file.ts",
"content": "// very-large-file.ts\n\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n",
"force": false
}
],
"enable_lazy_edits": true,
"enable_smart_files_context": true
}
},
"headers": {
"authorization": "Bearer testdyadkey"
}
}
\ No newline at end of file
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- textbox "src/**/*.config.ts"
- button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 2 files, ~\d+ tokens/
- button:
- img
\ No newline at end of file
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- textbox "src/**/*.config.ts"
- button "Add"
\ No newline at end of file
{
"body": {
"model": "gemini/gemini-2.5-pro-preview-05-06",
"max_tokens": 65535,
"temperature": 0,
"messages": [
{
"role": "system",
"content": "[[SYSTEM_MESSAGE]]"
},
{
"role": "user",
"content": "[dump]"
}
],
"stream": true,
"dyad_options": {
"files": [
{
"path": "a.ts",
"content": "// a.ts\n",
"force": true
},
{
"path": "manual/file.ts",
"content": "",
"force": true
},
{
"path": "manual/sub-manual/sub-manual.js",
"content": "",
"force": true
},
{
"path": "src/components/ui/helper.ts",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/foo.ts",
"content": "// foo.ts\n",
"force": false
},
{
"path": "src/sub/sub1.ts",
"content": "// sub/sub1.ts\n",
"force": false
},
{
"path": "src/sub/sub2.tsx",
"content": "// sub/sub2.tsx\n",
"force": false
},
{
"path": "src/very-large-file.ts",
"content": "// very-large-file.ts\n\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n",
"force": false
}
],
"enable_lazy_edits": true,
"enable_smart_files_context": true
}
},
"headers": {
"authorization": "Bearer testdyadkey"
}
}
\ No newline at end of file
- dialog:
- heading "Codebase Context" [level=3]
- paragraph:
- text: Select the files to use as context.
- img
- textbox "src/**/*.tsx"
- button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/
- button:
- img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/
- button:
- img
- heading "Smart Context Auto-includes" [level=3]
- paragraph:
- text: These files will always be included in the context.
- img
- textbox "src/**/*.config.ts"
- button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/
- button:
- img
- text: /manual\/\*\* 2 files, ~\d+ tokens/
- button:
- img
\ No newline at end of file
{
"body": {
"model": "gemini/gemini-2.5-pro-preview-05-06",
"max_tokens": 65535,
"temperature": 0,
"messages": [
{
"role": "system",
"content": "[[SYSTEM_MESSAGE]]"
},
{
"role": "user",
"content": "[dump]"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{
"role": "user",
"content": "[dump]"
}
],
"stream": true,
"dyad_options": {
"files": [
{
"path": "src/components/ui/helper.ts",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/foo.ts",
"content": "// foo.ts\n",
"force": false
},
{
"path": "src/sub/sub1.ts",
"content": "// sub/sub1.ts\n",
"force": false
},
{
"path": "src/sub/sub2.tsx",
"content": "// sub/sub2.tsx\n",
"force": false
},
{
"path": "src/very-large-file.ts",
"content": "// very-large-file.ts\n\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n",
"force": false
}
],
"enable_lazy_edits": true,
"enable_smart_files_context": false
}
},
"headers": {
"authorization": "Bearer testdyadkey"
}
}
\ No newline at end of file
{
"body": {
"model": "gemini/gemini-2.5-pro-preview-05-06",
"max_tokens": 65535,
"temperature": 0,
"messages": [
{
"role": "system",
"content": "[[SYSTEM_MESSAGE]]"
},
{
"role": "user",
"content": "[dump]"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{
"role": "user",
"content": "[dump]"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{
"role": "user",
"content": "[dump]"
}
],
"stream": true,
"dyad_options": {
"files": [
{
"path": "a.ts",
"content": "// a.ts\n",
"force": false
},
{
"path": "AI_RULES.md",
"content": "# AI_RULES.md\n",
"force": false
},
{
"path": "exclude/exclude.ts",
"content": "// exclude.ts: this file is not in any of the globs\n",
"force": false
},
{
"path": "exclude/exclude.tsx",
"content": "// exclude.tsx: this file is not in any of the globs\n",
"force": false
},
{
"path": "manual/file.ts",
"content": "",
"force": false
},
{
"path": "manual/sub-manual/sub-manual.js",
"content": "",
"force": false
},
{
"path": "src/components/ui/button.tsx",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/components/ui/helper.ts",
"content": "// Contents omitted for brevity",
"force": false
},
{
"path": "src/dir/some.css",
"content": "/* some.css */\n",
"force": false
},
{
"path": "src/foo.ts",
"content": "// foo.ts\n",
"force": false
},
{
"path": "src/sub/sub1.ts",
"content": "// sub/sub1.ts\n",
"force": false
},
{
"path": "src/sub/sub2.tsx",
"content": "// sub/sub2.tsx\n",
"force": false
},
{
"path": "src/very-large-file.ts",
"content": "// very-large-file.ts\n\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n// 1234567890\n",
"force": false
}
],
"enable_lazy_edits": true,
"enable_smart_files_context": false
}
},
"headers": {
"authorization": "Bearer testdyadkey"
}
}
\ No newline at end of file
差异被折叠。
......@@ -57,6 +57,7 @@
"@playwright/test": "^1.52.0",
"@testing-library/react": "^16.3.0",
"@types/better-sqlite3": "^7.6.13",
"@types/glob": "^8.1.0",
"@types/kill-port": "^2.0.3",
"@types/node": "^22.14.0",
"@types/react": "^19.0.10",
......@@ -89,6 +90,7 @@
"@openrouter/ai-sdk-provider": "^0.4.5",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.4",
......@@ -121,6 +123,7 @@
"fix-path": "^4.0.0",
"framer-motion": "^12.6.3",
"geist": "^1.3.1",
"glob": "^11.0.2",
"isomorphic-git": "^1.30.1",
"jotai": "^2.12.2",
"kill-port": "^2.0.1",
......
import { ContextFilesPicker } from "./ContextFilesPicker";
import { ModelPicker } from "./ModelPicker";
import { ProModeSelector } from "./ProModeSelector";
export function ChatInputControls() {
export function ChatInputControls({
showContextFilesPicker = false,
}: {
showContextFilesPicker?: boolean;
}) {
return (
<div className="pb-2 flex gap-2">
<ModelPicker />
{showContextFilesPicker && <ContextFilesPicker />}
<ProModeSelector />
</div>
);
......
差异被折叠。
......@@ -341,7 +341,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
)}
</div>
<div className="pl-2 pr-1 flex items-center justify-between">
<ChatInputControls />
<ChatInputControls showContextFilesPicker={true} />
<button
onClick={() => setShowTokenBar(!showTokenBar)}
className="flex items-center px-2 py-1 text-xs text-muted-foreground hover:bg-muted rounded"
......
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
......@@ -15,6 +15,7 @@ export const apps = sqliteTable("apps", {
githubOrg: text("github_org"),
githubRepo: text("github_repo"),
supabaseProjectId: text("supabase_project_id"),
chatContext: text("chat_context", { mode: "json" }),
});
export const chats = sqliteTable("chats", {
......
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { GlobPath, ContextPathResults } from "@/lib/schemas";
export function useContextPaths() {
const queryClient = useQueryClient();
const appId = useAtomValue(selectedAppIdAtom);
const {
data: contextPathsData,
isLoading,
error,
} = useQuery<ContextPathResults, Error>({
queryKey: ["context-paths", appId],
queryFn: async () => {
if (!appId) return { contextPaths: [], smartContextAutoIncludes: [] };
const ipcClient = IpcClient.getInstance();
return ipcClient.getChatContextResults({ appId });
},
enabled: !!appId,
});
const updateContextPathsMutation = useMutation<
unknown,
Error,
{ contextPaths: GlobPath[]; smartContextAutoIncludes?: GlobPath[] }
>({
mutationFn: async ({ contextPaths, smartContextAutoIncludes }) => {
if (!appId) throw new Error("No app selected");
const ipcClient = IpcClient.getInstance();
return ipcClient.setChatContext({
appId,
chatContext: {
contextPaths,
smartContextAutoIncludes: smartContextAutoIncludes || [],
},
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["context-paths", appId] });
},
});
const updateContextPaths = async (paths: GlobPath[]) => {
const currentAutoIncludes =
contextPathsData?.smartContextAutoIncludes || [];
return updateContextPathsMutation.mutateAsync({
contextPaths: paths,
smartContextAutoIncludes: currentAutoIncludes.map(({ globPath }) => ({
globPath,
})),
});
};
const updateSmartContextAutoIncludes = async (paths: GlobPath[]) => {
const currentContextPaths = contextPathsData?.contextPaths || [];
return updateContextPathsMutation.mutateAsync({
contextPaths: currentContextPaths.map(({ globPath }) => ({ globPath })),
smartContextAutoIncludes: paths,
});
};
return {
contextPaths: contextPathsData?.contextPaths || [],
smartContextAutoIncludes: contextPathsData?.smartContextAutoIncludes || [],
isLoading,
error,
updateContextPaths,
updateSmartContextAutoIncludes,
};
}
......@@ -33,6 +33,7 @@ import { readFile, writeFile, unlink } from "fs/promises";
import { getMaxTokens } from "../utils/token_utils";
import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants";
import { streamTextWithBackup } from "../utils/stream_utils";
import { validateChatContext } from "../utils/context_paths_utils";
const logger = log.scope("chat_stream_handlers");
......@@ -226,7 +227,10 @@ export function registerChatStreamHandlers() {
if (updatedChat.app) {
const appPath = getDyadAppPath(updatedChat.app.path);
try {
const out = await extractCodebase(appPath);
const out = await extractCodebase({
appPath,
chatContext: validateChatContext(updatedChat.app.chatContext),
});
codebaseInfo = out.formattedOutput;
files = out.files;
logger.log(`Extracted codebase information from ${appPath}`);
......
import { db } from "@/db";
import { apps } from "@/db/schema";
import { eq } from "drizzle-orm";
import { z } from "zod";
import {
AppChatContext,
AppChatContextSchema,
ContextPathResults,
} from "@/lib/schemas";
import { estimateTokens } from "../utils/token_utils";
import { createLoggedHandler } from "./safe_handle";
import log from "electron-log";
import { getDyadAppPath } from "@/paths/paths";
import { extractCodebase } from "@/utils/codebase";
import { validateChatContext } from "../utils/context_paths_utils";
const logger = log.scope("context_paths_handlers");
const handle = createLoggedHandler(logger);
export function registerContextPathsHandlers() {
handle(
"get-context-paths",
async (_, { appId }: { appId: number }): Promise<ContextPathResults> => {
z.object({ appId: z.number() }).parse({ appId });
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error("App not found");
}
if (!app.path) {
throw new Error("App path not set");
}
const appPath = getDyadAppPath(app.path);
const results: ContextPathResults = {
contextPaths: [],
smartContextAutoIncludes: [],
};
const { contextPaths, smartContextAutoIncludes } = validateChatContext(
app.chatContext,
);
for (const contextPath of contextPaths) {
const { formattedOutput, files } = await extractCodebase({
appPath,
chatContext: {
contextPaths: [contextPath],
smartContextAutoIncludes: [],
},
});
const totalTokens = estimateTokens(formattedOutput);
results.contextPaths.push({
...contextPath,
files: files.length,
tokens: totalTokens,
});
}
for (const contextPath of smartContextAutoIncludes) {
const { formattedOutput, files } = await extractCodebase({
appPath,
chatContext: {
contextPaths: [contextPath],
smartContextAutoIncludes: [],
},
});
const totalTokens = estimateTokens(formattedOutput);
results.smartContextAutoIncludes.push({
...contextPath,
files: files.length,
tokens: totalTokens,
});
}
return results;
},
);
handle(
"set-context-paths",
async (
_,
{ appId, chatContext }: { appId: number; chatContext: AppChatContext },
) => {
const schema = z.object({
appId: z.number(),
chatContext: AppChatContextSchema,
});
schema.parse({ appId, chatContext });
await db.update(apps).set({ chatContext }).where(eq(apps.id, appId));
},
);
}
......@@ -13,6 +13,7 @@ import { chats, apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import { LargeLanguageModel } from "@/lib/schemas";
import { validateChatContext } from "../utils/context_paths_utils";
// Shared function to get system debug info
async function getSystemDebugInfo({
......@@ -175,7 +176,12 @@ export function registerDebugHandlers() {
// Extract codebase
const appPath = getDyadAppPath(app.path);
const codebase = (await extractCodebase(appPath)).formattedOutput;
const codebase = (
await extractCodebase({
appPath,
chatContext: validateChatContext(app.chatContext),
})
).formattedOutput;
return {
debugInfo,
......
......@@ -31,6 +31,7 @@ import { getDyadAppPath } from "../../paths/paths";
import { withLock } from "../utils/lock_utils";
import { createLoggedHandler } from "./safe_handle";
import { ApproveProposalResult } from "../ipc_types";
import { validateChatContext } from "../utils/context_paths_utils";
const logger = log.scope("proposal_handlers");
const handle = createLoggedHandler(logger);
......@@ -41,6 +42,7 @@ interface CodebaseTokenCache {
messageContent: string;
tokenCount: number;
timestamp: number;
chatContext: string;
}
// Cache expiration time (5 minutes)
......@@ -74,6 +76,7 @@ async function getCodebaseTokenCount(
messageId: number,
messageContent: string,
appPath: string,
chatContext: unknown,
): Promise<number> {
// Clean up expired cache entries first
cleanupExpiredCacheEntries();
......@@ -86,6 +89,7 @@ async function getCodebaseTokenCount(
cacheEntry &&
cacheEntry.messageId === messageId &&
cacheEntry.messageContent === messageContent &&
cacheEntry.chatContext === JSON.stringify(chatContext) &&
now - cacheEntry.timestamp < CACHE_EXPIRATION_MS
) {
logger.log(`Using cached codebase token count for chatId: ${chatId}`);
......@@ -94,8 +98,12 @@ async function getCodebaseTokenCount(
// Calculate and cache the token count
logger.log(`Calculating codebase token count for chatId: ${chatId}`);
const codebase = (await extractCodebase(getDyadAppPath(appPath)))
.formattedOutput;
const codebase = (
await extractCodebase({
appPath: getDyadAppPath(appPath),
chatContext: validateChatContext(chatContext),
})
).formattedOutput;
const tokenCount = estimateTokens(codebase);
// Store in cache
......@@ -105,6 +113,7 @@ async function getCodebaseTokenCount(
messageContent,
tokenCount,
timestamp: now,
chatContext: JSON.stringify(chatContext),
});
return tokenCount;
......@@ -277,6 +286,7 @@ const getProposalHandler = async (
latestAssistantMessage.id,
latestAssistantMessage.content || "",
chat.app.path,
chat.app.chatContext,
);
const totalTokens = messagesTokenCount + codebaseTokenCount;
......
......@@ -18,6 +18,7 @@ import { TokenCountParams } from "../ipc_types";
import { TokenCountResult } from "../ipc_types";
import { estimateTokens, getContextWindow } from "../utils/token_utils";
import { createLoggedHandler } from "./safe_handle";
import { validateChatContext } from "../utils/context_paths_utils";
const logger = log.scope("token_count_handlers");
......@@ -73,7 +74,12 @@ export function registerTokenCountHandlers() {
if (chat.app) {
const appPath = getDyadAppPath(chat.app.path);
codebaseInfo = (await extractCodebase(appPath)).formattedOutput;
codebaseInfo = (
await extractCodebase({
appPath,
chatContext: validateChatContext(chat.app.chatContext),
})
).formattedOutput;
codebaseTokens = estimateTokens(codebaseInfo);
logger.log(
`Extracted codebase information from ${appPath}, tokens: ${codebaseTokens}`,
......
......@@ -3,9 +3,9 @@ import {
type ChatSummary,
ChatSummariesSchema,
type UserSettings,
type ContextPathResults,
} from "../lib/schemas";
import type {
App,
AppOutput,
Chat,
ChatResponseEnd,
......@@ -32,8 +32,9 @@ import type {
RenameBranchParams,
UserBudgetInfo,
CopyAppParams,
App,
} from "./ipc_types";
import type { ProposalResult } from "@/lib/schemas";
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast";
export interface ChatStreamCallbacks {
......@@ -847,4 +848,17 @@ export class IpcClient {
public async getUserBudget(): Promise<UserBudgetInfo | null> {
return this.ipcRenderer.invoke("get-user-budget");
}
public async getChatContextResults(params: {
appId: number;
}): Promise<ContextPathResults> {
return this.ipcRenderer.invoke("get-context-paths", params);
}
public async setChatContext(params: {
appId: number;
chatContext: AppChatContext;
}): Promise<void> {
return this.ipcRenderer.invoke("set-context-paths", params);
}
}
......@@ -19,6 +19,7 @@ import { registerReleaseNoteHandlers } from "./handlers/release_note_handlers";
import { registerImportHandlers } from "./handlers/import_handlers";
import { registerSessionHandlers } from "./handlers/session_handlers";
import { registerProHandlers } from "./handlers/pro_handlers";
import { registerContextPathsHandlers } from "./handlers/context_paths_handlers";
export function registerIpcHandlers() {
// Register all IPC handlers by category
......@@ -43,4 +44,5 @@ export function registerIpcHandlers() {
registerImportHandlers();
registerSessionHandlers();
registerProHandlers();
registerContextPathsHandlers();
}
import { AppChatContext, AppChatContextSchema } from "@/lib/schemas";
import log from "electron-log";
const logger = log.scope("context_paths_utils");
export function validateChatContext(chatContext: unknown): AppChatContext {
if (!chatContext) {
return {
contextPaths: [],
smartContextAutoIncludes: [],
};
}
try {
// Validate that the contextPaths data matches the expected schema
return AppChatContextSchema.parse(chatContext);
} catch (error) {
logger.warn("Invalid contextPaths data:", error);
// Return empty array as fallback if validation fails
return {
contextPaths: [],
smartContextAutoIncludes: [],
};
}
}
......@@ -100,6 +100,28 @@ export const DyadProBudgetSchema = z.object({
});
export type DyadProBudget = z.infer<typeof DyadProBudgetSchema>;
export const GlobPathSchema = z.object({
globPath: z.string(),
});
export type GlobPath = z.infer<typeof GlobPathSchema>;
export const AppChatContextSchema = z.object({
contextPaths: z.array(GlobPathSchema),
smartContextAutoIncludes: z.array(GlobPathSchema),
});
export type AppChatContext = z.infer<typeof AppChatContextSchema>;
export type ContextPathResult = GlobPath & {
files: number;
tokens: number;
};
export type ContextPathResults = {
contextPaths: ContextPathResult[];
smartContextAutoIncludes: ContextPathResult[];
};
/**
* Zod schema for user settings
*/
......
......@@ -77,6 +77,8 @@ const validInvokeChannels = [
"rename-branch",
"clear-session-data",
"get-user-budget",
"get-context-paths",
"set-context-paths",
// Test-only channels
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
// We can't detect with IS_TEST_BUILD in the preload script because
......
......@@ -4,6 +4,9 @@ import path from "node:path";
import { isIgnored } from "isomorphic-git";
import log from "electron-log";
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
import { glob } from "glob";
import { AppChatContext } from "../lib/schemas";
import { readSettings } from "@/main/settings";
const logger = log.scope("utils/codebase");
......@@ -315,15 +318,31 @@ ${content}
}
}
export type CodebaseFile = {
path: string;
content: string;
force?: boolean;
};
/**
* Extract and format codebase files as a string to be included in prompts
* @param appPath - Path to the codebase to extract
* @returns Object containing formatted output and individual files
*/
export async function extractCodebase(appPath: string): Promise<{
export async function extractCodebase({
appPath,
chatContext,
}: {
appPath: string;
chatContext: AppChatContext;
}): Promise<{
formattedOutput: string;
files: { path: string; content: string }[];
files: CodebaseFile[];
}> {
const settings = readSettings();
const isSmartContextEnabled =
settings?.enableDyadPro && settings?.enableProSmartFilesContextMode;
try {
await fsAsync.access(appPath);
} catch {
......@@ -335,14 +354,67 @@ export async function extractCodebase(appPath: string): Promise<{
const startTime = Date.now();
// Collect all relevant files
const files = await collectFiles(appPath, appPath);
let files = await collectFiles(appPath, appPath);
// Collect files from contextPaths and smartContextAutoIncludes
const { contextPaths, smartContextAutoIncludes } = chatContext;
const includedFiles = new Set<string>();
const autoIncludedFiles = new Set<string>();
// Add files from contextPaths
if (contextPaths && contextPaths.length > 0) {
for (const p of contextPaths) {
const pattern = createFullGlobPath({
appPath,
globPath: p.globPath,
});
const matches = await glob(pattern, {
nodir: true,
absolute: true,
ignore: "**/node_modules/**",
});
matches.forEach((file) => {
const normalizedFile = path.normalize(file);
includedFiles.add(normalizedFile);
});
}
}
// Add files from smartContextAutoIncludes
if (
isSmartContextEnabled &&
smartContextAutoIncludes &&
smartContextAutoIncludes.length > 0
) {
for (const p of smartContextAutoIncludes) {
const pattern = createFullGlobPath({
appPath,
globPath: p.globPath,
});
const matches = await glob(pattern, {
nodir: true,
absolute: true,
});
matches.forEach((file) => {
const normalizedFile = path.normalize(file);
autoIncludedFiles.add(normalizedFile);
includedFiles.add(normalizedFile); // Also add to included files
});
}
}
// Only filter files if contextPaths are provided
// If only smartContextAutoIncludes are provided, keep all files and just mark auto-includes as forced
if (contextPaths && contextPaths.length > 0) {
files = files.filter((file) => includedFiles.has(path.normalize(file)));
}
// Sort files by modification time (oldest first)
// This is important for cache-ability.
const sortedFiles = await sortFilesByModificationTime(files);
const sortedFiles = await sortFilesByModificationTime([...new Set(files)]);
// Format files and collect individual file contents
const filesArray: { path: string; content: string }[] = [];
const filesArray: CodebaseFile[] = [];
const formatPromises = sortedFiles.map(async (file) => {
const formattedContent = await formatFile(file, appPath);
......@@ -352,6 +424,9 @@ export async function extractCodebase(appPath: string): Promise<{
// Why? Normalize Windows-style paths which causes lots of weird issues (e.g. Git commit)
.split(path.sep)
.join("/");
const isForced = autoIncludedFiles.has(path.normalize(file));
const fileContent = isOmittedFile(relativePath)
? OMITTED_FILE_CONTENT
: await readFileWithCache(file);
......@@ -359,6 +434,7 @@ export async function extractCodebase(appPath: string): Promise<{
filesArray.push({
path: relativePath,
content: fileContent,
force: isForced,
});
}
......@@ -413,3 +489,15 @@ async function sortFilesByModificationTime(files: string[]): Promise<string[]> {
// Sort by modification time (oldest first)
return fileStats.sort((a, b) => a.mtime - b.mtime).map((item) => item.file);
}
function createFullGlobPath({
appPath,
globPath,
}: {
appPath: string;
globPath: string;
}): string {
// By default the glob package treats "\" as an escape character.
// We want the path to use forward slash for all platforms.
return `${appPath.replace(/\\/g, "/")}/${globPath}`;
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论