Click to edit UI (#385)
- [x] add e2e test - happy case (make sure it clears selection and next prompt is empty, and preview is cleared); de-selection case - [x] shim - old & new file - [x] upgrade path - [x] add docs - [x] add try-catch to parser script - [x] make it work for next.js - [x] extract npm package - [x] make sure plugin doesn't apply in prod
This commit is contained in:
@@ -442,6 +442,8 @@ export function registerAppHandlers() {
|
||||
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
try {
|
||||
// Kill any orphaned process on port 32100 (in case previous run left it)
|
||||
await killProcessOnPort(32100);
|
||||
await executeApp({ appPath, appId, event });
|
||||
|
||||
return;
|
||||
|
||||
179
src/ipc/handlers/app_upgrade_handlers.ts
Normal file
179
src/ipc/handlers/app_upgrade_handlers.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import log from "electron-log";
|
||||
import { AppUpgrade } from "../ipc_types";
|
||||
import { db } from "../../db";
|
||||
import { apps } from "../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getDyadAppPath } from "../../paths/paths";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const logger = log.scope("app_upgrade_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
|
||||
const availableUpgrades: Omit<AppUpgrade, "isNeeded">[] = [
|
||||
{
|
||||
id: "component-tagger",
|
||||
title: "Enable select component to edit",
|
||||
description:
|
||||
"Installs the Dyad component tagger Vite plugin and its dependencies.",
|
||||
manualUpgradeUrl: "https://dyad.sh/docs/upgrades/select-component",
|
||||
},
|
||||
];
|
||||
|
||||
async function getApp(appId: number) {
|
||||
const app = await db.query.apps.findFirst({
|
||||
where: eq(apps.id, appId),
|
||||
});
|
||||
if (!app) {
|
||||
throw new Error(`App with id ${appId} not found`);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
function isComponentTaggerUpgradeNeeded(appPath: string): boolean {
|
||||
const viteConfigPathJs = path.join(appPath, "vite.config.js");
|
||||
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
|
||||
|
||||
let viteConfigPath;
|
||||
if (fs.existsSync(viteConfigPathTs)) {
|
||||
viteConfigPath = viteConfigPathTs;
|
||||
} else if (fs.existsSync(viteConfigPathJs)) {
|
||||
viteConfigPath = viteConfigPathJs;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const viteConfigContent = fs.readFileSync(viteConfigPath, "utf-8");
|
||||
return !viteConfigContent.includes("@dyad-sh/react-vite-component-tagger");
|
||||
} catch (e) {
|
||||
logger.error("Error reading vite config", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyComponentTagger(appPath: string) {
|
||||
const viteConfigPathJs = path.join(appPath, "vite.config.js");
|
||||
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
|
||||
|
||||
let viteConfigPath;
|
||||
if (fs.existsSync(viteConfigPathTs)) {
|
||||
viteConfigPath = viteConfigPathTs;
|
||||
} else if (fs.existsSync(viteConfigPathJs)) {
|
||||
viteConfigPath = viteConfigPathJs;
|
||||
} else {
|
||||
throw new Error("Could not find vite.config.js or vite.config.ts");
|
||||
}
|
||||
|
||||
let content = await fs.promises.readFile(viteConfigPath, "utf-8");
|
||||
|
||||
// Add import statement if not present
|
||||
if (
|
||||
!content.includes(
|
||||
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
|
||||
)
|
||||
) {
|
||||
// Add it after the last import statement
|
||||
const lines = content.split("\n");
|
||||
let lastImportIndex = -1;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].startsWith("import ")) {
|
||||
lastImportIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines.splice(
|
||||
lastImportIndex + 1,
|
||||
0,
|
||||
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
|
||||
);
|
||||
content = lines.join("\n");
|
||||
}
|
||||
|
||||
// Add plugin to plugins array
|
||||
if (content.includes("plugins: [")) {
|
||||
if (!content.includes("dyadComponentTagger()")) {
|
||||
content = content.replace(
|
||||
"plugins: [",
|
||||
"plugins: [dyadComponentTagger(), ",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"Could not find `plugins: [` in vite.config.ts. Manual installation required.",
|
||||
);
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(viteConfigPath, content);
|
||||
|
||||
// Install the dependency
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
logger.info("Installing component-tagger dependency");
|
||||
const process = spawn(
|
||||
"pnpm add -D @dyad-sh/react-vite-component-tagger || npm install --save-dev --legacy-peer-deps @dyad-sh/react-vite-component-tagger",
|
||||
{
|
||||
cwd: appPath,
|
||||
shell: true,
|
||||
stdio: "pipe",
|
||||
},
|
||||
);
|
||||
|
||||
process.stdout?.on("data", (data) => logger.info(data.toString()));
|
||||
process.stderr?.on("data", (data) => logger.error(data.toString()));
|
||||
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
logger.info("component-tagger dependency installed successfully");
|
||||
resolve();
|
||||
} else {
|
||||
logger.error(`Failed to install dependency, exit code ${code}`);
|
||||
reject(new Error("Failed to install dependency"));
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", (err) => {
|
||||
logger.error("Failed to spawn pnpm", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerAppUpgradeHandlers() {
|
||||
handle(
|
||||
"get-app-upgrades",
|
||||
async (_, { appId }: { appId: number }): Promise<AppUpgrade[]> => {
|
||||
const app = await getApp(appId);
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
const upgradesWithStatus = availableUpgrades.map((upgrade) => {
|
||||
let isNeeded = false;
|
||||
if (upgrade.id === "component-tagger") {
|
||||
isNeeded = isComponentTaggerUpgradeNeeded(appPath);
|
||||
}
|
||||
return { ...upgrade, isNeeded };
|
||||
});
|
||||
|
||||
return upgradesWithStatus;
|
||||
},
|
||||
);
|
||||
|
||||
handle(
|
||||
"execute-app-upgrade",
|
||||
async (_, { appId, upgradeId }: { appId: number; upgradeId: string }) => {
|
||||
if (!upgradeId) {
|
||||
throw new Error("upgradeId is required");
|
||||
}
|
||||
|
||||
const app = await getApp(appId);
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
if (upgradeId === "component-tagger") {
|
||||
await applyComponentTagger(appPath);
|
||||
} else {
|
||||
throw new Error(`Unknown upgrade id: ${upgradeId}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -160,7 +160,45 @@ export function registerChatStreamHandlers() {
|
||||
}
|
||||
|
||||
// Add user message to database with attachment info
|
||||
const userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
|
||||
let userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
|
||||
if (req.selectedComponent) {
|
||||
let componentSnippet = "[component snippet not available]";
|
||||
try {
|
||||
const componentFileContent = await readFile(
|
||||
path.join(
|
||||
getDyadAppPath(chat.app.path),
|
||||
req.selectedComponent.relativePath,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const lines = componentFileContent.split("\n");
|
||||
const selectedIndex = req.selectedComponent.lineNumber - 1;
|
||||
|
||||
// Let's get one line before and three after for context.
|
||||
const startIndex = Math.max(0, selectedIndex - 1);
|
||||
const endIndex = Math.min(lines.length, selectedIndex + 4);
|
||||
|
||||
const snippetLines = lines.slice(startIndex, endIndex);
|
||||
const selectedLineInSnippetIndex = selectedIndex - startIndex;
|
||||
|
||||
if (snippetLines[selectedLineInSnippetIndex]) {
|
||||
snippetLines[selectedLineInSnippetIndex] =
|
||||
`${snippetLines[selectedLineInSnippetIndex]} // <-- EDIT HERE`;
|
||||
}
|
||||
|
||||
componentSnippet = snippetLines.join("\n");
|
||||
} catch (err) {
|
||||
logger.error(`Error reading selected component file content: ${err}`);
|
||||
}
|
||||
|
||||
userPrompt += `\n\nSelected component: ${req.selectedComponent.name} (file: ${req.selectedComponent.relativePath})
|
||||
|
||||
Snippet:
|
||||
\`\`\`
|
||||
${componentSnippet}
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
await db
|
||||
.insert(messages)
|
||||
.values({
|
||||
@@ -228,7 +266,16 @@ export function registerChatStreamHandlers() {
|
||||
try {
|
||||
const out = await extractCodebase({
|
||||
appPath,
|
||||
chatContext: validateChatContext(updatedChat.app.chatContext),
|
||||
chatContext: req.selectedComponent
|
||||
? {
|
||||
contextPaths: [
|
||||
{
|
||||
globPath: req.selectedComponent.relativePath,
|
||||
},
|
||||
],
|
||||
smartContextAutoIncludes: [],
|
||||
}
|
||||
: validateChatContext(updatedChat.app.chatContext),
|
||||
});
|
||||
codebaseInfo = out.formattedOutput;
|
||||
files = out.files;
|
||||
|
||||
Reference in New Issue
Block a user