Skip to content

Commit aeb7588

Browse files
committed
React + FastAPI working fine in full stack terminal - enhanced terminal integration for all modes
- Implement side-by-side terminal layout in Preview tab for better monitoring - Add intelligent terminal command routing based on chat mode (ask/frontend/backend/fullstack) - Fix terminal command parsing to handle AI-generated cmd: prefixes - Auto-start backend and frontend servers in development modes with proper terminal feedback - Ensure both frontend and backend terminals appear when creating apps in Fullstack/Backend modes - Route general terminal commands to appropriate terminals based on mode and command type
1 parent db58cd3 commit aeb7588

File tree

4 files changed

+177
-80
lines changed

4 files changed

+177
-80
lines changed

src/components/backend-chat/BackendChatInput.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,26 @@ export function BackendChatInput({ chatId }: { chatId?: number }) {
100100
handlePaste,
101101
} = useAttachments();
102102

103-
// Auto-start servers when entering backend or fullstack mode
103+
// Initialize terminals and auto-start servers when entering backend or fullstack mode
104104
useEffect(() => {
105-
const startServers = async () => {
105+
const initializeTerminalsAndStartServers = async () => {
106106
if (!appId) return;
107107

108108
try {
109+
// Import terminal output functions
110+
const { addTerminalOutput } = await import("../../ipc/handlers/terminal_handlers");
111+
112+
// Initialize backend terminal with welcome message
113+
addTerminalOutput(appId, "backend", `🚀 Backend Development Environment Ready`, "output");
114+
addTerminalOutput(appId, "backend", `Type commands or ask me to run backend operations...`, "output");
115+
116+
// For fullstack mode, also initialize frontend terminal
117+
if (settings?.selectedChatMode === "fullstack") {
118+
addTerminalOutput(appId, "frontend", `🚀 Frontend Development Environment Ready`, "output");
119+
addTerminalOutput(appId, "frontend", `Type commands or ask me to run frontend operations...`, "output");
120+
}
121+
122+
// Start servers after initializing terminals
109123
// Always start backend server for backend/fullstack modes
110124
logger.info(`Auto-starting backend server for app: ${appId}`);
111125
await IpcClient.getInstance().startBackendServer(appId);
@@ -118,12 +132,12 @@ export function BackendChatInput({ chatId }: { chatId?: number }) {
118132
logger.info("Frontend server started successfully");
119133
}
120134
} catch (error) {
121-
logger.error("Failed to auto-start servers:", error);
135+
logger.error("Failed to initialize terminals or auto-start servers:", error);
122136
}
123137
};
124138

125139
// Small delay to ensure component is fully mounted
126-
const timeoutId = setTimeout(startServers, 1000);
140+
const timeoutId = setTimeout(initializeTerminalsAndStartServers, 500);
127141
return () => clearTimeout(timeoutId);
128142
}, [appId, settings?.selectedChatMode]);
129143

Lines changed: 90 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,106 @@
1-
import { appOutputAtom, frontendTerminalOutputAtom, backendTerminalOutputAtom, activeTerminalAtom } from "@/atoms/appAtoms";
2-
import { useAtomValue, useSetAtom } from "jotai";
1+
import { appOutputAtom, frontendTerminalOutputAtom, backendTerminalOutputAtom } from "@/atoms/appAtoms";
2+
import { useAtomValue } from "jotai";
33

4-
// Console component with multi-terminal support
4+
// Console component with side-by-side terminal support
55
export const Console = () => {
66
const appOutput = useAtomValue(appOutputAtom);
77
const frontendTerminalOutput = useAtomValue(frontendTerminalOutputAtom);
88
const backendTerminalOutput = useAtomValue(backendTerminalOutputAtom);
9-
const activeTerminal = useAtomValue(activeTerminalAtom);
10-
const setActiveTerminal = useSetAtom(activeTerminalAtom);
119

12-
// Determine which output to show based on active terminal
13-
const getCurrentOutput = () => {
14-
switch (activeTerminal) {
15-
case "frontend":
16-
return frontendTerminalOutput;
17-
case "backend":
18-
return backendTerminalOutput;
19-
case "main":
20-
default:
21-
return appOutput;
22-
}
23-
};
24-
25-
const currentOutput = getCurrentOutput();
10+
// Determine which terminals to show
11+
const hasFrontend = frontendTerminalOutput.length > 0;
12+
const hasBackend = backendTerminalOutput.length > 0;
13+
const hasMain = appOutput.length > 0;
2614

27-
// Show terminal tabs only in fullstack mode (when we have multiple terminals)
28-
const showTabs = frontendTerminalOutput.length > 0 || backendTerminalOutput.length > 0;
15+
// Count active terminals
16+
const activeTerminals = [hasMain && "main", hasFrontend && "frontend", hasBackend && "backend"].filter(Boolean);
17+
const terminalCount = activeTerminals.length;
2918

30-
return (
19+
// Terminal rendering component
20+
const TerminalPanel = ({ title, outputs, color }: { title: string; outputs: any[]; color: string }) => (
3121
<div className="flex flex-col h-full">
32-
{showTabs && (
33-
<div className="flex border-b border-border bg-[var(--background)]">
34-
<button
35-
onClick={() => setActiveTerminal("main")}
36-
className={`px-3 py-1 text-xs font-medium ${
37-
activeTerminal === "main"
38-
? "border-b-2 border-blue-500 text-blue-500"
39-
: "text-gray-500 hover:text-gray-700"
40-
}`}
41-
>
42-
System ({appOutput.length})
43-
</button>
44-
{frontendTerminalOutput.length > 0 && (
45-
<button
46-
onClick={() => setActiveTerminal("frontend")}
47-
className={`px-3 py-1 text-xs font-medium ${
48-
activeTerminal === "frontend"
49-
? "border-b-2 border-green-500 text-green-500"
50-
: "text-gray-500 hover:text-gray-700"
51-
}`}
52-
>
53-
Frontend ({frontendTerminalOutput.length})
54-
</button>
55-
)}
56-
{backendTerminalOutput.length > 0 && (
57-
<button
58-
onClick={() => setActiveTerminal("backend")}
59-
className={`px-3 py-1 text-xs font-medium ${
60-
activeTerminal === "backend"
61-
? "border-b-2 border-orange-500 text-orange-500"
62-
: "text-gray-500 hover:text-gray-700"
63-
}`}
64-
>
65-
Backend ({backendTerminalOutput.length})
66-
</button>
67-
)}
68-
</div>
69-
)}
22+
<div className={`px-3 py-2 bg-${color}-100 dark:bg-${color}-900 text-${color}-800 dark:text-${color}-200 text-xs font-medium border-b border-border`}>
23+
{title} ({outputs.length})
24+
</div>
7025
<div className="font-mono text-xs px-4 flex-1 overflow-auto">
71-
{currentOutput.map((output, index) => (
26+
{outputs.map((output, index) => (
7227
<div key={index}>{output.message}</div>
7328
))}
7429
</div>
7530
</div>
7631
);
32+
33+
// Single terminal layout
34+
if (terminalCount === 1) {
35+
if (hasFrontend) {
36+
return <TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />;
37+
}
38+
if (hasBackend) {
39+
return <TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />;
40+
}
41+
return <TerminalPanel title="System" outputs={appOutput} color="blue" />;
42+
}
43+
44+
// Two terminals layout
45+
if (terminalCount === 2) {
46+
if (hasFrontend && hasBackend) {
47+
// Frontend and Backend side by side
48+
return (
49+
<div className="flex h-full">
50+
<div className="flex-1 border-r border-border">
51+
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
52+
</div>
53+
<div className="flex-1">
54+
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
55+
</div>
56+
</div>
57+
);
58+
}
59+
if (hasMain && hasFrontend) {
60+
// System and Frontend side by side
61+
return (
62+
<div className="flex h-full">
63+
<div className="flex-1 border-r border-border">
64+
<TerminalPanel title="System" outputs={appOutput} color="blue" />
65+
</div>
66+
<div className="flex-1">
67+
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
68+
</div>
69+
</div>
70+
);
71+
}
72+
if (hasMain && hasBackend) {
73+
// System and Backend side by side
74+
return (
75+
<div className="flex h-full">
76+
<div className="flex-1 border-r border-border">
77+
<TerminalPanel title="System" outputs={appOutput} color="blue" />
78+
</div>
79+
<div className="flex-1">
80+
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
81+
</div>
82+
</div>
83+
);
84+
}
85+
}
86+
87+
// Three terminals layout - show in a 3-column layout
88+
if (terminalCount === 3) {
89+
return (
90+
<div className="flex h-full">
91+
<div className="flex-1 border-r border-border">
92+
<TerminalPanel title="System" outputs={appOutput} color="blue" />
93+
</div>
94+
<div className="flex-1 border-r border-border">
95+
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
96+
</div>
97+
<div className="flex-1">
98+
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
99+
</div>
100+
</div>
101+
);
102+
}
103+
104+
// Fallback - show system terminal
105+
return <TerminalPanel title="System" outputs={appOutput} color="blue" />;
77106
};

src/ipc/handlers/chat_handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ipcMain } from "electron";
22
import { db } from "../../db";
3-
import { startBackendServer } from "./createFromTemplate";
3+
import { startBackendServer, startFrontendServer } from "./createFromTemplate";
44
import { apps, chats, messages } from "../../db/schema";
55
import { desc, eq, and, like } from "drizzle-orm";
66
import type { ChatSearchResult, ChatSummary } from "../../lib/schemas";

src/ipc/processors/response_processor.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ export async function processFullResponseActions(
132132
const dyadRunFrontendTerminalCmdTags = getDyadRunFrontendTerminalCmdTags(fullResponse);
133133
const dyadRunTerminalCmdTags = getDyadRunTerminalCmdTags(fullResponse);
134134

135+
// Determine the chat mode to route general terminal commands appropriately
136+
let chatMode = settings.selectedChatMode;
137+
if (chatWithApp.app) {
138+
// Check if there's a backend directory to determine if this is backend/fullstack mode
139+
const backendPath = path.join(appPath, "backend");
140+
const hasBackend = fs.existsSync(backendPath);
141+
if (hasBackend) {
142+
chatMode = settings.selectedChatMode === "fullstack" ? "fullstack" : "backend";
143+
}
144+
}
145+
135146
const message = await db.query.messages.findFirst({
136147
where: and(
137148
eq(messages.id, messageId),
@@ -268,41 +279,84 @@ export async function processFullResponseActions(
268279
logger.log(`Executed ${dyadRunFrontendTerminalCmdTags.length} frontend terminal commands`);
269280
}
270281

271-
// Handle general terminal command tags
282+
// Handle general terminal command tags - route based on chat mode
272283
if (dyadRunTerminalCmdTags.length > 0) {
273284
for (const cmdTag of dyadRunTerminalCmdTags) {
285+
// Clean up the command - remove any "cmd:" prefix that AI might add
286+
let cleanCommand = cmdTag.command.trim();
287+
if (cleanCommand.startsWith("cmd:")) {
288+
cleanCommand = cleanCommand.substring(4).trim();
289+
}
290+
if (cleanCommand.startsWith("command:")) {
291+
cleanCommand = cleanCommand.substring(8).trim();
292+
}
293+
274294
try {
275-
const cwd = cmdTag.cwd ? path.join(appPath, cmdTag.cwd) : appPath;
276295

277-
logger.log(`Executing general terminal command: ${cmdTag.command} in ${cwd}`);
296+
// Determine which terminal to route to based on chat mode
297+
let terminalType: "frontend" | "backend" = "backend"; // default
298+
let cwd = cmdTag.cwd ? path.join(appPath, cmdTag.cwd) : appPath;
299+
300+
if (chatMode === "ask") {
301+
// For ask mode, route to frontend terminal (most common for general commands)
302+
terminalType = "frontend";
303+
if (!cmdTag.cwd) {
304+
cwd = path.join(appPath, "frontend");
305+
}
306+
} else if (chatMode === "backend") {
307+
terminalType = "backend";
308+
// For backend mode, adjust cwd to backend directory if not already specified
309+
if (!cmdTag.cwd) {
310+
cwd = path.join(appPath, "backend");
311+
}
312+
} else if (chatMode === "fullstack") {
313+
// For fullstack mode, try to determine based on command content or default to backend
314+
// Commands related to frontend development go to frontend terminal
315+
const frontendCommands = ["npm", "yarn", "pnpm", "vite", "next", "react", "webpack"];
316+
const isFrontendCommand = frontendCommands.some(cmd => cleanCommand.toLowerCase().includes(cmd));
317+
318+
if (isFrontendCommand) {
319+
terminalType = "frontend";
320+
if (!cmdTag.cwd) {
321+
cwd = path.join(appPath, "frontend");
322+
}
323+
} else {
324+
terminalType = "backend";
325+
if (!cmdTag.cwd) {
326+
cwd = path.join(appPath, "backend");
327+
}
328+
}
329+
}
330+
331+
logger.log(`Executing general terminal command: ${cleanCommand} in ${cwd} (routing to ${terminalType} terminal)`);
278332

279-
const result = await runShellCommand(`cd "${cwd}" && ${cmdTag.command}`);
333+
const result = await runShellCommand(`cd "${cwd}" && ${cleanCommand}`);
280334

281335
if (result === null) {
282336
errors.push({
283-
message: `Terminal command failed: ${cmdTag.description || cmdTag.command}`,
337+
message: `Terminal command failed: ${cmdTag.description || cleanCommand}`,
284338
error: `Command execution failed in ${cwd}`,
285339
});
286-
// Add error to backend terminal
287-
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${cmdTag.description || cmdTag.command}`, "error");
340+
// Add error to appropriate terminal
341+
addTerminalOutput(chatWithApp.app.id, terminalType, `❌ Error: ${cmdTag.description || cleanCommand}`, "error");
288342
} else {
289-
logger.log(`Terminal command succeeded: ${cmdTag.description || cmdTag.command}`);
343+
logger.log(`Terminal command succeeded: ${cmdTag.description || cleanCommand}`);
290344

291-
// Add command and result to backend terminal
292-
addTerminalOutput(chatWithApp.app.id, "backend", `$ ${cmdTag.command}`, "command");
345+
// Add command and result to appropriate terminal
346+
addTerminalOutput(chatWithApp.app.id, terminalType, `$ ${cleanCommand}`, "command");
293347

294348
if (result.trim()) {
295-
addTerminalOutput(chatWithApp.app.id, "backend", result, "output");
349+
addTerminalOutput(chatWithApp.app.id, terminalType, result, "output");
296350
}
297351

298-
addTerminalOutput(chatWithApp.app.id, "backend", `✅ ${cmdTag.description || cmdTag.command} completed successfully`, "success");
352+
addTerminalOutput(chatWithApp.app.id, terminalType, `✅ ${cmdTag.description || cleanCommand} completed successfully`, "success");
299353
}
300354
} catch (error) {
301355
errors.push({
302-
message: `Terminal command failed: ${cmdTag.description || cmdTag.command}`,
356+
message: `Terminal command failed: ${cmdTag.description || cleanCommand}`,
303357
error: error,
304358
});
305-
// Add error to backend terminal
359+
// Add error to appropriate terminal (default to backend for errors)
306360
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${error}`, "error");
307361
}
308362
}

0 commit comments

Comments
 (0)