Skip to content

Commit 0c9a82a

Browse files
committed
feat: implement multi-terminal UI for Full Stack mode
- Add frontend and backend terminal output atoms to track separate terminal sessions - Add activeTerminalAtom to manage which terminal is currently displayed - Update Console component with tabbed interface for System, Frontend, and Backend terminals - Add frontend terminal command processing in response_processor.ts - Add terminal output coloring (cyan for commands, green for success, red for errors) - Implement automatic terminal switching when new output is added - Update fullstack system prompt to use both frontend and backend terminal command tags - Commands now execute automatically in appropriate directories and results are displayed in respective terminals
1 parent aff33f5 commit 0c9a82a

File tree

7 files changed

+258
-32
lines changed

7 files changed

+258
-32
lines changed

src/atoms/appAtoms.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const previewModeAtom = atom<
1212
>("preview");
1313
export const selectedVersionIdAtom = atom<string | null>(null);
1414
export const appOutputAtom = atom<AppOutput[]>([]);
15+
export const frontendTerminalOutputAtom = atom<AppOutput[]>([]);
16+
export const backendTerminalOutputAtom = atom<AppOutput[]>([]);
17+
export const activeTerminalAtom = atom<"main" | "frontend" | "backend">("main");
1518
export const appUrlAtom = atom<
1619
| { appUrl: string; appId: number; originalUrl: string }
1720
| { appUrl: null; appId: null; originalUrl: null }
Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,77 @@
1-
import { appOutputAtom } from "@/atoms/appAtoms";
2-
import { useAtomValue } from "jotai";
1+
import { appOutputAtom, frontendTerminalOutputAtom, backendTerminalOutputAtom, activeTerminalAtom } from "@/atoms/appAtoms";
2+
import { useAtomValue, useSetAtom } from "jotai";
33

4-
// Console component
4+
// Console component with multi-terminal support
55
export const Console = () => {
66
const appOutput = useAtomValue(appOutputAtom);
7+
const frontendTerminalOutput = useAtomValue(frontendTerminalOutputAtom);
8+
const backendTerminalOutput = useAtomValue(backendTerminalOutputAtom);
9+
const activeTerminal = useAtomValue(activeTerminalAtom);
10+
const setActiveTerminal = useSetAtom(activeTerminalAtom);
11+
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();
26+
27+
// Show terminal tabs only in fullstack mode (when we have multiple terminals)
28+
const showTabs = frontendTerminalOutput.length > 0 || backendTerminalOutput.length > 0;
29+
730
return (
8-
<div className="font-mono text-xs px-4 h-full overflow-auto">
9-
{appOutput.map((output, index) => (
10-
<div key={index}>{output.message}</div>
11-
))}
31+
<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+
)}
70+
<div className="font-mono text-xs px-4 flex-1 overflow-auto">
71+
{currentOutput.map((output, index) => (
72+
<div key={index}>{output.message}</div>
73+
))}
74+
</div>
1275
</div>
1376
);
1477
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { safeSend } from "../utils/safe_sender";
2+
import { frontendTerminalOutputAtom, backendTerminalOutputAtom, activeTerminalAtom } from "../../atoms/appAtoms";
3+
import { getDefaultStore } from "jotai";
4+
import log from "electron-log";
5+
6+
const logger = log.scope("terminal_handlers");
7+
8+
export function registerTerminalHandlers() {
9+
// No IPC handlers needed - this module handles terminal output routing
10+
}
11+
12+
// Function to add output to a specific terminal
13+
export function addTerminalOutput(appId: number, terminal: "frontend" | "backend", message: string, type: "command" | "output" | "success" | "error" = "output") {
14+
const store = getDefaultStore();
15+
16+
// Format message with timestamp and type indicator
17+
const timestamp = new Date().toLocaleTimeString();
18+
let formattedMessage = `[${timestamp}] ${message}`;
19+
20+
// Add type-specific formatting
21+
if (type === "command") {
22+
formattedMessage = `\x1b[36m${formattedMessage}\x1b[0m`; // Cyan for commands
23+
} else if (type === "success") {
24+
formattedMessage = `\x1b[32m${formattedMessage}\x1b[0m`; // Green for success
25+
} else if (type === "error") {
26+
formattedMessage = `\x1b[31m${formattedMessage}\x1b[0m`; // Red for errors
27+
}
28+
29+
// Map our types to AppOutput types
30+
let appOutputType: "stdout" | "stderr" | "info" | "client-error" | "input-requested";
31+
switch (type) {
32+
case "error":
33+
appOutputType = "stderr";
34+
break;
35+
case "success":
36+
case "command":
37+
appOutputType = "info";
38+
break;
39+
default:
40+
appOutputType = "stdout";
41+
}
42+
43+
const outputItem = {
44+
message: formattedMessage,
45+
timestamp: Date.now(),
46+
type: appOutputType,
47+
appId
48+
};
49+
50+
if (terminal === "frontend") {
51+
const currentOutput = store.get(frontendTerminalOutputAtom);
52+
store.set(frontendTerminalOutputAtom, [...currentOutput, outputItem]);
53+
54+
// Auto-switch to frontend terminal if it's empty
55+
if (currentOutput.length === 0) {
56+
store.set(activeTerminalAtom, "frontend");
57+
}
58+
} else if (terminal === "backend") {
59+
const currentOutput = store.get(backendTerminalOutputAtom);
60+
store.set(backendTerminalOutputAtom, [...currentOutput, outputItem]);
61+
62+
// Auto-switch to backend terminal if it's empty
63+
if (currentOutput.length === 0) {
64+
store.set(activeTerminalAtom, "backend");
65+
}
66+
}
67+
68+
logger.log(`Added ${type} output to ${terminal} terminal: ${message}`);
69+
}

src/ipc/ipc_host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { registerTemplateHandlers } from "./handlers/template_handlers";
3030
import { registerPortalHandlers } from "./handlers/portal_handlers";
3131
import { registerPromptHandlers } from "./handlers/prompt_handlers";
3232
import { registerHelpBotHandlers } from "./handlers/help_bot_handlers";
33+
import { registerTerminalHandlers } from "./handlers/terminal_handlers";
3334

3435
export function registerIpcHandlers() {
3536
// Register all IPC handlers by category
@@ -65,4 +66,5 @@ export function registerIpcHandlers() {
6566
registerPortalHandlers();
6667
registerPromptHandlers();
6768
registerHelpBotHandlers();
69+
registerTerminalHandlers();
6870
}

src/ipc/processors/response_processor.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import {
2727
getDyadAddDependencyTags,
2828
getDyadExecuteSqlTags,
2929
getDyadRunBackendTerminalCmdTags,
30+
getDyadRunFrontendTerminalCmdTags,
3031
} from "../utils/dyad_tag_parser";
3132
import { runShellCommand } from "../utils/runShellCommand";
3233
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
3334

3435
import { FileUploadsState } from "../utils/file_uploads_state";
36+
import { addTerminalOutput } from "../handlers/terminal_handlers";
3537

3638
const readFile = fs.promises.readFile;
3739
const logger = log.scope("response_processor");
@@ -122,6 +124,7 @@ export async function processFullResponseActions(
122124
? getDyadExecuteSqlTags(fullResponse)
123125
: [];
124126
const dyadRunBackendTerminalCmdTags = getDyadRunBackendTerminalCmdTags(fullResponse);
127+
const dyadRunFrontendTerminalCmdTags = getDyadRunFrontendTerminalCmdTags(fullResponse);
125128

126129
const message = await db.query.messages.findFirst({
127130
where: and(
@@ -180,28 +183,85 @@ export async function processFullResponseActions(
180183

181184
logger.log(`Executing backend terminal command: ${cmdTag.command} in ${cwd}`);
182185

186+
// Add command to backend terminal output
187+
// We need to import and use the backendTerminalOutputAtom
188+
// For now, we'll use a more direct approach by sending IPC messages to update the UI
189+
183190
const result = await runShellCommand(`cd "${cwd}" && ${cmdTag.command}`);
184191

185192
if (result === null) {
186193
errors.push({
187194
message: `Backend terminal command failed: ${cmdTag.description || cmdTag.command}`,
188195
error: `Command execution failed in ${cwd}`,
189196
});
197+
// Add error to backend terminal
198+
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${cmdTag.description || cmdTag.command}`, "error");
190199
} else {
191200
logger.log(`Backend terminal command succeeded: ${cmdTag.description || cmdTag.command}`);
192-
// Add success message to app output
193-
// Note: We don't add to writtenFiles since these are not file changes
201+
202+
// Add command and result to backend terminal
203+
addTerminalOutput(chatWithApp.app.id, "backend", `$ ${cmdTag.command}`, "command");
204+
205+
if (result.trim()) {
206+
addTerminalOutput(chatWithApp.app.id, "backend", result, "output");
207+
}
208+
209+
addTerminalOutput(chatWithApp.app.id, "backend", `✅ ${cmdTag.description || cmdTag.command} completed successfully`, "success");
194210
}
195211
} catch (error) {
196212
errors.push({
197213
message: `Backend terminal command failed: ${cmdTag.description || cmdTag.command}`,
198214
error: error,
199215
});
216+
// Add error to backend terminal
217+
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${error}`, "error");
200218
}
201219
}
202220
logger.log(`Executed ${dyadRunBackendTerminalCmdTags.length} backend terminal commands`);
203221
}
204222

223+
// Handle frontend terminal command tags
224+
if (dyadRunFrontendTerminalCmdTags.length > 0) {
225+
for (const cmdTag of dyadRunFrontendTerminalCmdTags) {
226+
try {
227+
const frontendPath = path.join(appPath, "frontend");
228+
const cwd = cmdTag.cwd ? path.join(frontendPath, cmdTag.cwd) : frontendPath;
229+
230+
logger.log(`Executing frontend terminal command: ${cmdTag.command} in ${cwd}`);
231+
232+
const result = await runShellCommand(`cd "${cwd}" && ${cmdTag.command}`);
233+
234+
if (result === null) {
235+
errors.push({
236+
message: `Frontend terminal command failed: ${cmdTag.description || cmdTag.command}`,
237+
error: `Command execution failed in ${cwd}`,
238+
});
239+
// Add error to frontend terminal
240+
addTerminalOutput(chatWithApp.app.id, "frontend", `❌ Error: ${cmdTag.description || cmdTag.command}`, "error");
241+
} else {
242+
logger.log(`Frontend terminal command succeeded: ${cmdTag.description || cmdTag.command}`);
243+
244+
// Add command and result to frontend terminal
245+
addTerminalOutput(chatWithApp.app.id, "frontend", `$ ${cmdTag.command}`, "command");
246+
247+
if (result.trim()) {
248+
addTerminalOutput(chatWithApp.app.id, "frontend", result, "output");
249+
}
250+
251+
addTerminalOutput(chatWithApp.app.id, "frontend", `✅ ${cmdTag.description || cmdTag.command} completed successfully`, "success");
252+
}
253+
} catch (error) {
254+
errors.push({
255+
message: `Frontend terminal command failed: ${cmdTag.description || cmdTag.command}`,
256+
error: error,
257+
});
258+
// Add error to frontend terminal
259+
addTerminalOutput(chatWithApp.app.id, "frontend", `❌ Error: ${error}`, "error");
260+
}
261+
}
262+
logger.log(`Executed ${dyadRunFrontendTerminalCmdTags.length} frontend terminal commands`);
263+
}
264+
205265
// Handle add dependency tags
206266
if (dyadAddDependencyPackages.length > 0) {
207267
try {

src/ipc/utils/dyad_tag_parser.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,32 @@ export function getDyadRunBackendTerminalCmdTags(fullResponse: string): {
166166

167167
return commands;
168168
}
169+
170+
export function getDyadRunFrontendTerminalCmdTags(fullResponse: string): {
171+
command: string;
172+
cwd?: string;
173+
description?: string;
174+
}[] {
175+
const dyadRunFrontendTerminalCmdRegex =
176+
/<dyad-run-frontend-terminal-cmd([^>]*)>([\s\S]*?)<\/dyad-run-frontend-terminal-cmd>/g;
177+
const cwdRegex = /cwd="([^"]+)"/;
178+
const descriptionRegex = /description="([^"]+)"/;
179+
180+
let match;
181+
const commands: { command: string; cwd?: string; description?: string }[] = [];
182+
183+
while ((match = dyadRunFrontendTerminalCmdRegex.exec(fullResponse)) !== null) {
184+
const attributesString = match[1];
185+
const command = match[2].trim();
186+
187+
const cwdMatch = cwdRegex.exec(attributesString);
188+
const descriptionMatch = descriptionRegex.exec(attributesString);
189+
190+
const cwd = cwdMatch?.[1];
191+
const description = descriptionMatch?.[1];
192+
193+
commands.push({ command, cwd, description });
194+
}
195+
196+
return commands;
197+
}

src/prompts/system_prompt.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -468,37 +468,37 @@ const FULLSTACK_AI_RULES = `# Full Stack Development
468468
## Framework-Specific Terminal Commands
469469
470470
### Django Commands (Backend Terminal)
471-
- Install dependencies: \`pip install -r requirements.txt\`
472-
- Run migrations: \`python manage.py migrate\`
473-
- Create migrations: \`python manage.py makemigrations\`
474-
- Start server: \`python manage.py runserver 8000\`
475-
- Create superuser: \`python manage.py createsuperuser\`
476-
- Collect static files: \`python manage.py collectstatic\`
471+
- Install dependencies: <dyad-run-backend-terminal-cmd description="Install Python dependencies">pip install -r requirements.txt</dyad-run-backend-terminal-cmd>
472+
- Run migrations: <dyad-run-backend-terminal-cmd description="Apply database migrations">python manage.py migrate</dyad-run-backend-terminal-cmd>
473+
- Create migrations: <dyad-run-backend-terminal-cmd description="Create database migrations">python manage.py makemigrations</dyad-run-backend-terminal-cmd>
474+
- Start server: <dyad-run-backend-terminal-cmd description="Start Django development server">python manage.py runserver 8000</dyad-run-backend-terminal-cmd>
475+
- Create superuser: <dyad-run-backend-terminal-cmd description="Create Django superuser">python manage.py createsuperuser</dyad-run-backend-terminal-cmd>
476+
- Collect static files: <dyad-run-backend-terminal-cmd description="Collect static files">python manage.py collectstatic</dyad-run-backend-terminal-cmd>
477477
478478
### FastAPI Commands (Backend Terminal)
479-
- Install dependencies: \`pip install -r requirements.txt\`
480-
- Start server: \`uvicorn main:app --reload --host 0.0.0.0 --port 8000\`
481-
- Run with hot reload: \`uvicorn main:app --reload\`
482-
- Generate client: \`python -c "from main import app; print(app.openapi())"\`
479+
- Install dependencies: <dyad-run-backend-terminal-cmd description="Install Python dependencies">pip install -r requirements.txt</dyad-run-backend-terminal-cmd>
480+
- Start server: <dyad-run-backend-terminal-cmd description="Start FastAPI server">uvicorn main:app --reload --host 0.0.0.0 --port 8000</dyad-run-backend-terminal-cmd>
481+
- Run with hot reload: <dyad-run-backend-terminal-cmd description="Run FastAPI with hot reload">uvicorn main:app --reload</dyad-run-backend-terminal-cmd>
482+
- Generate client: <dyad-run-backend-terminal-cmd description="Generate OpenAPI client">python -c "from main import app; print(app.openapi())"</dyad-run-backend-terminal-cmd>
483483
484484
### Flask Commands (Backend Terminal)
485-
- Install dependencies: \`pip install -r requirements.txt\`
486-
- Start server: \`python app.py\`
487-
- Start with Flask CLI: \`flask run --host=0.0.0.0 --port=5000\`
488-
- Initialize database: \`flask db init\` (if using Flask-Migrate)
485+
- Install dependencies: <dyad-run-backend-terminal-cmd description="Install Python dependencies">pip install -r requirements.txt</dyad-run-backend-terminal-cmd>
486+
- Start server: <dyad-run-backend-terminal-cmd description="Start Flask development server">python app.py</dyad-run-backend-terminal-cmd>
487+
- Start with Flask CLI: <dyad-run-backend-terminal-cmd description="Start Flask with CLI">flask run --host=0.0.0.0 --port=5000</dyad-run-backend-terminal-cmd>
488+
- Initialize database: <dyad-run-backend-terminal-cmd description="Initialize Flask-Migrate">flask db init</dyad-run-backend-terminal-cmd>
489489
490490
### Node.js Commands (Backend Terminal)
491-
- Install dependencies: \`npm install\`
492-
- Start server: \`npm start\` or \`node server.js\`
493-
- Development mode: \`npm run dev\` or \`nodemon server.js\`
494-
- Build for production: \`npm run build\`
491+
- Install dependencies: <dyad-run-backend-terminal-cmd description="Install Node.js dependencies">npm install</dyad-run-backend-terminal-cmd>
492+
- Start server: <dyad-run-backend-terminal-cmd description="Start production server">npm start</dyad-run-backend-terminal-cmd>
493+
- Development mode: <dyad-run-backend-terminal-cmd description="Start development server">npm run dev</dyad-run-backend-terminal-cmd>
494+
- Build for production: <dyad-run-backend-terminal-cmd description="Build for production">npm run build</dyad-run-backend-terminal-cmd>
495495
496496
### Frontend Commands (Frontend Terminal)
497-
- Install dependencies: \`npm install\`
498-
- Start development server: \`npm run dev\`
499-
- Build for production: \`npm run build\`
500-
- Run tests: \`npm test\`
501-
- Lint code: \`npm run lint\`
497+
- Install dependencies: <dyad-run-frontend-terminal-cmd description="Install frontend dependencies">npm install</dyad-run-frontend-terminal-cmd>
498+
- Start development server: <dyad-run-frontend-terminal-cmd description="Start frontend development server">npm run dev</dyad-run-frontend-terminal-cmd>
499+
- Build for production: <dyad-run-frontend-terminal-cmd description="Build frontend for production">npm run build</dyad-run-frontend-terminal-cmd>
500+
- Run tests: <dyad-run-frontend-terminal-cmd description="Run frontend tests">npm test</dyad-run-frontend-terminal-cmd>
501+
- Lint code: <dyad-run-frontend-terminal-cmd description="Lint frontend code">npm run lint</dyad-run-frontend-terminal-cmd>
502502
503503
## Integration Best Practices
504504
- Design clean API contracts between frontend and backend

0 commit comments

Comments
 (0)