Skip to content

Commit dd63906

Browse files
committed
feat: Enhanced backend development with independent system messages and auto-start servers
1 parent 1dc509e commit dd63906

File tree

14 files changed

+589
-66
lines changed

14 files changed

+589
-66
lines changed

scaffold/AI_RULES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
Available packages and libraries:
1414

1515
- The lucide-react package is installed for icons.
16+
- The date-fns package is installed for date/time manipulation.
17+
- The uuid package is installed for generating unique identifiers.
18+
- The leaflet and react-leaflet packages are installed for interactive maps.
19+
- The axios package is installed for HTTP requests.
1620
- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.
1721
- You have ALL the necessary Radix UI components installed.
1822
- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.

scaffold/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@
4040
"@radix-ui/react-toggle-group": "^1.1.0",
4141
"@radix-ui/react-tooltip": "^1.1.4",
4242
"@tanstack/react-query": "^5.56.2",
43+
"axios": "^1.7.4",
4344
"class-variance-authority": "^0.7.1",
4445
"clsx": "^2.1.1",
4546
"cmdk": "^1.0.0",
46-
"date-fns": "^3.6.0",
47+
"date-fns": "^4.1.0",
4748
"embla-carousel-react": "^8.3.0",
4849
"input-otp": "^1.2.4",
50+
"leaflet": "^1.9.4",
4951
"lucide-react": "^0.462.0",
5052
"next-themes": "^0.3.0",
53+
"react-leaflet": "^4.2.1",
5154
"react": "^18.3.1",
5255
"react-day-picker": "^8.10.1",
5356
"react-dom": "^18.3.1",
@@ -58,25 +61,28 @@
5861
"sonner": "^1.5.0",
5962
"tailwind-merge": "^2.5.2",
6063
"tailwindcss-animate": "^1.0.7",
64+
"typescript": "^5.5.3",
65+
"uuid": "^10.0.0",
6166
"vaul": "^0.9.3",
6267
"zod": "^3.23.8"
6368
},
6469
"devDependencies": {
6570
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
6671
"@eslint/js": "^9.9.0",
6772
"@tailwindcss/typography": "^0.5.15",
73+
"@types/leaflet": "^1.9.12",
6874
"@types/node": "^22.5.5",
6975
"@types/react": "^18.3.3",
7076
"@types/react-dom": "^18.3.0",
71-
"@vitejs/plugin-react-swc": "^3.9.0",
77+
"@types/uuid": "^10.0.0",
78+
"@vitejs/plugin-react": "^4.3.1",
7279
"autoprefixer": "^10.4.20",
7380
"eslint": "^9.9.0",
7481
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
7582
"eslint-plugin-react-refresh": "^0.4.9",
7683
"globals": "^15.9.0",
7784
"postcss": "^8.4.47",
7885
"tailwindcss": "^3.4.11",
79-
"typescript": "^5.5.3",
8086
"typescript-eslint": "^8.0.1",
8187
"vite": "^6.3.4"
8288
}

scaffold/vite.config.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { defineConfig } from "vite";
2-
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
3-
import react from "@vitejs/plugin-react-swc";
2+
import react from "@vitejs/plugin-react";
43
import path from "path";
54

65
export default defineConfig(() => ({
76
server: {
87
host: "::",
98
port: 8080,
109
},
11-
plugins: [dyadComponentTagger(), react()],
10+
plugins: [react()],
1211
resolve: {
1312
alias: {
1413
"@": path.resolve(__dirname, "./src"),

src/components/backend-chat/BackendChatInput.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "lucide-react";
2121
import type React from "react";
2222
import { useState, useEffect, useCallback } from "react";
23+
import log from "electron-log";
2324

2425
import { useSettings } from "@/hooks/useSettings";
2526
import { IpcClient } from "@/ipc/ipc_client";
@@ -66,6 +67,7 @@ import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
6667
import { SelectedComponentDisplay } from "../chat/SelectedComponentDisplay";
6768

6869
const showTokenBarAtom = atom(false);
70+
const logger = log.scope("BackendChatInput");
6971

7072
export function BackendChatInput({ chatId }: { chatId?: number }) {
7173
const posthog = usePostHog();
@@ -98,6 +100,25 @@ export function BackendChatInput({ chatId }: { chatId?: number }) {
98100
handlePaste,
99101
} = useAttachments();
100102

103+
// Auto-start backend server when entering backend mode
104+
useEffect(() => {
105+
const startBackendServer = async () => {
106+
if (!appId) return;
107+
108+
try {
109+
logger.info(`Auto-starting backend server for app: ${appId}`);
110+
await IpcClient.getInstance().startBackendServer(appId);
111+
logger.info("Backend server started successfully");
112+
} catch (error) {
113+
logger.error("Failed to auto-start backend server:", error);
114+
}
115+
};
116+
117+
// Small delay to ensure component is fully mounted
118+
const timeoutId = setTimeout(startBackendServer, 1000);
119+
return () => clearTimeout(timeoutId);
120+
}, [appId]);
121+
101122
// Use the hook to fetch the proposal
102123
const {
103124
proposalResult,

src/components/chat/MessagesList.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ interface MessagesListProps {
2626

2727
export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
2828
function MessagesList({ messages, messagesEndRef }, ref) {
29+
// Ensure messages is always an array to prevent null reference errors
30+
const safeMessages = messages || [];
2931
const appId = useAtomValue(selectedAppIdAtom);
3032
const { versions, revertVersion } = useVersions(appId);
3133
const { streamMessage, isStreaming } = useStreamChat();
@@ -58,12 +60,12 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
5860
ref={ref}
5961
data-testid="messages-list"
6062
>
61-
{messages.length > 0
62-
? messages.map((message, index) => (
63+
{safeMessages.length > 0
64+
? safeMessages.map((message, index) => (
6365
<ChatMessage
6466
key={index}
6567
message={message}
66-
isLastMessage={index === messages.length - 1}
68+
isLastMessage={index === safeMessages.length - 1}
6769
/>
6870
))
6971
: !renderSetupBanner() && (
@@ -75,9 +77,9 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
7577
)}
7678
{!isStreaming && (
7779
<div className="flex max-w-3xl mx-auto gap-2">
78-
{!!messages.length &&
79-
messages[messages.length - 1].role === "assistant" &&
80-
messages[messages.length - 1].commitHash && (
80+
{!!safeMessages.length &&
81+
safeMessages[safeMessages.length - 1].role === "assistant" &&
82+
safeMessages[safeMessages.length - 1].commitHash && (
8183
<Button
8284
variant="outline"
8385
size="sm"
@@ -90,9 +92,9 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
9092

9193
setIsUndoLoading(true);
9294
try {
93-
if (messages.length >= 3) {
95+
if (safeMessages.length >= 3) {
9496
const previousAssistantMessage =
95-
messages[messages.length - 3];
97+
safeMessages[safeMessages.length - 3];
9698
if (
9799
previousAssistantMessage?.role === "assistant" &&
98100
previousAssistantMessage?.commitHash
@@ -108,8 +110,8 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
108110
selectedChatId,
109111
);
110112
setMessages(chat.messages);
111-
}
112-
} else {
113+
}
114+
} else {
113115
const chat =
114116
await IpcClient.getInstance().getChat(selectedChatId);
115117
if (chat.initialCommitHash) {
@@ -146,7 +148,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
146148
Undo
147149
</Button>
148150
)}
149-
{!!messages.length && (
151+
{!!safeMessages.length && (
150152
<Button
151153
variant="outline"
152154
size="sm"
@@ -161,14 +163,14 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
161163
try {
162164
// The last message is usually an assistant, but it might not be.
163165
const lastVersion = versions[0];
164-
const lastMessage = messages[messages.length - 1];
166+
const lastMessage = safeMessages[safeMessages.length - 1];
165167
let shouldRedo = true;
166168
if (
167169
lastVersion.oid === lastMessage.commitHash &&
168170
lastMessage.role === "assistant"
169171
) {
170172
const previousAssistantMessage =
171-
messages[messages.length - 3];
173+
safeMessages[safeMessages.length - 3];
172174
if (
173175
previousAssistantMessage?.role === "assistant" &&
174176
previousAssistantMessage?.commitHash
@@ -200,7 +202,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
200202
}
201203

202204
// Find the last user message
203-
const lastUserMessage = [...messages]
205+
const lastUserMessage = [...safeMessages]
204206
.reverse()
205207
.find((message) => message.role === "user");
206208
if (!lastUserMessage) {
@@ -238,9 +240,9 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
238240
{isStreaming &&
239241
!settings?.enableDyadPro &&
240242
!userBudget &&
241-
messages.length > 0 && (
243+
safeMessages.length > 0 && (
242244
<PromoMessage
243-
seed={messages.length * (appId ?? 1) * (selectedChatId ?? 1)}
245+
seed={safeMessages.length * (appId ?? 1) * (selectedChatId ?? 1)}
244246
/>
245247
)}
246248
<div ref={messagesEndRef} />

src/components/chat/monaco.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,86 @@ export const customDark: editor.IStandaloneThemeData = {
180180

181181
editor.defineTheme("dyad-dark", customDark);
182182

183+
// Function to dispose of Monaco models for a specific file path
184+
export function disposeMonacoModel(filePath: string) {
185+
try {
186+
const uri = monaco.Uri.file(filePath);
187+
const model = monaco.editor.getModel(uri);
188+
if (model) {
189+
model.dispose();
190+
console.log(`Disposed Monaco model for ${filePath}`);
191+
}
192+
} catch (error) {
193+
console.warn(`Failed to dispose Monaco model for ${filePath}:`, error);
194+
}
195+
}
196+
197+
// Function to invalidate Monaco models for a directory (useful when many files change)
198+
export function invalidateMonacoModelsForDirectory(directoryPath: string) {
199+
try {
200+
const models = monaco.editor.getModels();
201+
models.forEach(model => {
202+
const modelPath = model.uri.path;
203+
if (modelPath.startsWith(directoryPath)) {
204+
model.dispose();
205+
console.log(`Disposed Monaco model for ${modelPath}`);
206+
}
207+
});
208+
} catch (error) {
209+
console.warn(`Failed to invalidate Monaco models for directory ${directoryPath}:`, error);
210+
}
211+
}
212+
213+
// Function to create or update a Monaco model for a file
214+
export function ensureMonacoModel(filePath: string, content: string, language?: string) {
215+
try {
216+
const uri = monaco.Uri.file(filePath);
217+
let model = monaco.editor.getModel(uri);
218+
219+
if (model) {
220+
// Update existing model
221+
model.setValue(content);
222+
} else {
223+
// Create new model
224+
const detectedLanguage = language || getLanguageFromPath(filePath);
225+
model = monaco.editor.createModel(content, detectedLanguage, uri);
226+
}
227+
228+
return model;
229+
} catch (error) {
230+
console.warn(`Failed to ensure Monaco model for ${filePath}:`, error);
231+
return null;
232+
}
233+
}
234+
235+
// Helper function to detect language from file path
236+
function getLanguageFromPath(filePath: string): string {
237+
const extension = filePath.split('.').pop()?.toLowerCase();
238+
const languageMap: Record<string, string> = {
239+
js: 'javascript',
240+
jsx: 'javascript',
241+
ts: 'typescript',
242+
tsx: 'typescript',
243+
html: 'html',
244+
css: 'css',
245+
json: 'json',
246+
md: 'markdown',
247+
py: 'python',
248+
java: 'java',
249+
c: 'c',
250+
cpp: 'cpp',
251+
cs: 'csharp',
252+
go: 'go',
253+
rs: 'rust',
254+
rb: 'ruby',
255+
php: 'php',
256+
swift: 'swift',
257+
kt: 'kotlin',
258+
};
259+
260+
return languageMap[extension || ''] || 'plaintext';
261+
}
262+
183263
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
184264
jsx: monaco.languages.typescript.JsxEmit.React, // Enable JSX
185265
});

src/components/preview_panel/FileEditor.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Editor, { OnMount } from "@monaco-editor/react";
33
import { useLoadAppFile } from "@/hooks/useLoadAppFile";
44
import { useTheme } from "@/contexts/ThemeContext";
55
import { ChevronRight, Circle, Save } from "lucide-react";
6-
import "@/components/chat/monaco";
76
import { IpcClient } from "@/ipc/ipc_client";
87
import { showError, showSuccess, showWarning } from "@/lib/toast";
98
import { Button } from "@/components/ui/button";
@@ -102,9 +101,16 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
102101
const queryClient = useQueryClient();
103102
const { checkProblems } = useCheckProblems(appId);
104103

105-
// Update state when content loads
104+
// Update state when content loads or file path changes
106105
useEffect(() => {
107106
if (content !== null) {
107+
// Dispose of old model if file path changed
108+
if (originalValueRef.current !== undefined && typeof window !== "undefined") {
109+
import("@/components/chat/monaco").then(({ disposeMonacoModel }) => {
110+
disposeMonacoModel(filePath);
111+
}).catch(err => console.warn("Failed to dispose Monaco model:", err));
112+
}
113+
108114
setValue(content);
109115
originalValueRef.current = content;
110116
currentValueRef.current = content;
@@ -123,13 +129,21 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
123129
const isDarkMode =
124130
theme === "dark" ||
125131
(theme === "system" &&
132+
typeof window !== "undefined" &&
126133
window.matchMedia("(prefers-color-scheme: dark)").matches);
127134
const editorTheme = isDarkMode ? "dyad-dark" : "dyad-light";
128135

129136
// Handle editor mount
130137
const handleEditorDidMount: OnMount = (editor) => {
131138
editorRef.current = editor;
132139

140+
// Ensure Monaco model exists for this file
141+
if (currentValueRef.current !== undefined && typeof window !== "undefined") {
142+
import("@/components/chat/monaco").then(({ ensureMonacoModel }) => {
143+
ensureMonacoModel(filePath, currentValueRef.current!);
144+
}).catch(err => console.warn("Failed to ensure Monaco model:", err));
145+
}
146+
133147
// Listen for model content change events
134148
editor.onDidBlurEditorText(() => {
135149
console.log("Editor text blurred, checking if save needed");
@@ -169,6 +183,14 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
169183
filePath,
170184
currentValueRef.current,
171185
);
186+
187+
// Update Monaco model to ensure it's in sync
188+
if (typeof window !== "undefined") {
189+
import("@/components/chat/monaco").then(({ ensureMonacoModel }) => {
190+
ensureMonacoModel(filePath, currentValueRef.current!);
191+
}).catch(err => console.warn("Failed to update Monaco model:", err));
192+
}
193+
172194
await queryClient.invalidateQueries({ queryKey: ["versions", appId] });
173195
if (settings?.enableAutoFixProblems) {
174196
checkProblems();

0 commit comments

Comments
 (0)