Skip to content

Commit 289d7c4

Browse files
committed
Fix backend terminal commands failing in fullstack mode
- Add command prefix stripping logic for backend/frontend commands in fullstack mode - Commands like 'python3 backend/manage.py migrate' now properly execute as 'python3 manage.py migrate' from the backend directory - Fixes Django management commands and other framework-specific commands in fullstack development mode
1 parent 89f83c8 commit 289d7c4

File tree

8 files changed

+345
-29
lines changed

8 files changed

+345
-29
lines changed

e2e-tests/log_routing.spec.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { test, expect } from "@playwright/test";
2+
import {
3+
createApp,
4+
deleteApp,
5+
startApp,
6+
stopApp,
7+
getAppOutput,
8+
getTerminalOutput,
9+
switchTerminal,
10+
createBackendFile,
11+
createFrontendFile,
12+
} from "./helpers/test_helper";
13+
14+
test.describe("Log Routing and Visibility", () => {
15+
const appName = "test-log-routing-app";
16+
let appId: number;
17+
18+
test.beforeAll(async ({ page }) => {
19+
// Create a new app for testing
20+
appId = await createApp(page, appName, {
21+
isFullStack: true,
22+
selectedBackendFramework: "flask",
23+
});
24+
});
25+
26+
test.afterAll(async ({ page }) => {
27+
// Clean up the app after tests
28+
await deleteApp(page, appId);
29+
});
30+
31+
test("should stream all logs to system terminal and UI terminals in FullStack mode", async ({
32+
page,
33+
}) => {
34+
test.setTimeout(120000); // Increase timeout for app startup
35+
36+
// Create a backend file that produces stdout and stderr
37+
await createBackendFile(
38+
page,
39+
appId,
40+
"app.py",
41+
`
42+
from flask import Flask, jsonify
43+
import sys
44+
import time
45+
46+
app = Flask(__name__)
47+
48+
@app.route('/')
49+
def hello():
50+
print("Backend stdout: Hello from Flask!")
51+
sys.stderr.write("Backend stderr: This is an error message.\\n")
52+
return jsonify({"message": "Backend API is running!"})
53+
54+
if __name__ == '__main__':
55+
# Simulate a startup error for a moment, then recover
56+
print("Backend starting...")
57+
time.sleep(2) # Simulate some startup time
58+
# This will cause an error if a non-existent module is imported
59+
# try:
60+
# import non_existent_module
61+
# except ImportError:
62+
# sys.stderr.write("Backend stderr: Failed to import non_existent_module.\\n")
63+
app.run(debug=True, host='0.0.0.0', port=5000)
64+
`,
65+
);
66+
67+
// Create a frontend file that makes a request to the backend
68+
await createFrontendFile(
69+
page,
70+
appId,
71+
"src/App.tsx",
72+
`
73+
import React, { useEffect, useState } from 'react';
74+
75+
function App() {
76+
const [message, setMessage] = useState('Loading...');
77+
78+
useEffect(() => {
79+
console.log("Frontend stdout: App started.");
80+
fetch('http://localhost:5000/')
81+
.then(res => res.json())
82+
.then(data => {
83+
setMessage(data.message);
84+
console.log("Frontend stdout: Backend message received.");
85+
})
86+
.catch(error => {
87+
console.error("Frontend stderr: Error fetching from backend:", error);
88+
setMessage('Error: ' + error.message);
89+
});
90+
}, []);
91+
92+
return (
93+
<div className="App">
94+
<h1>Todo App</h1>
95+
<p>{message}</p>
96+
</div>
97+
);
98+
}
99+
100+
export default App;
101+
`,
102+
);
103+
104+
// Start the app in FullStack mode
105+
await startApp(page, appId, "main");
106+
107+
// Wait for some output to appear in the system messages
108+
await page.waitForSelector(".system-messages-container", { state: "visible" });
109+
await page.waitForTimeout(10000); // Give time for logs to accumulate
110+
111+
// Check if logs appear in the system console (Playwright can't directly access Node.js console,
112+
// but we can check the UI's system messages which should reflect console logs)
113+
const systemMessages = await getAppOutput(page, appId);
114+
expect(systemMessages).toContain("Backend stdout: Hello from Flask!");
115+
expect(systemMessages).toContain("Backend stderr: This is an error message.");
116+
expect(systemMessages).toContain("Frontend stdout: App started.");
117+
expect(systemMessages).toContain("Frontend stdout: Backend message received.");
118+
119+
// Switch to backend terminal and check logs
120+
await switchTerminal(page, "backend");
121+
const backendTerminalOutput = await getTerminalOutput(page, appId, "backend");
122+
expect(backendTerminalOutput).toContain("Backend stdout: Hello from Flask!");
123+
expect(backendTerminalOutput).toContain("Backend stderr: This is an error message.");
124+
125+
// Switch to frontend terminal and check logs
126+
await switchTerminal(page, "frontend");
127+
const frontendTerminalOutput = await getTerminalOutput(page, appId, "frontend");
128+
expect(frontendTerminalOutput).toContain("Frontend stdout: App started.");
129+
expect(frontendTerminalOutput).toContain("Frontend stdout: Backend message received.");
130+
expect(frontendTerminalOutput).toContain("Backend stdout: Hello from Flask!"); // Should also see backend logs in frontend for fullstack
131+
expect(frontendTerminalOutput).toContain("Backend stderr: This is an error message."); // Should also see backend errors in frontend for fullstack
132+
133+
await stopApp(page, appId);
134+
});
135+
136+
test("should stream logs to system terminal and frontend terminal in Frontend mode", async ({
137+
page,
138+
}) => {
139+
test.setTimeout(60000);
140+
141+
// Start the app in Frontend mode
142+
await startApp(page, appId, "frontend");
143+
await page.waitForTimeout(5000); // Give time for logs
144+
145+
const systemMessages = await getAppOutput(page, appId);
146+
expect(systemMessages).toContain("Frontend stdout: App started.");
147+
expect(systemMessages).not.toContain("Backend stdout:"); // Should not contain backend logs
148+
149+
await switchTerminal(page, "frontend");
150+
const frontendTerminalOutput = await getTerminalOutput(page, appId, "frontend");
151+
expect(frontendTerminalOutput).toContain("Frontend stdout: App started.");
152+
expect(frontendTerminalOutput).not.toContain("Backend stdout:");
153+
154+
await stopApp(page, appId);
155+
});
156+
157+
test("should stream logs to system terminal and backend terminal in Backend mode", async ({
158+
page,
159+
}) => {
160+
test.setTimeout(60000);
161+
162+
// Start the app in Backend mode
163+
await startApp(page, appId, "backend");
164+
await page.waitForTimeout(5000); // Give time for logs
165+
166+
const systemMessages = await getAppOutput(page, appId);
167+
expect(systemMessages).toContain("Backend stdout: Hello from Flask!");
168+
expect(systemMessages).toContain("Backend stderr: This is an error message.");
169+
expect(systemMessages).not.toContain("Frontend stdout:"); // Should not contain frontend logs
170+
171+
await switchTerminal(page, "backend");
172+
const backendTerminalOutput = await getTerminalOutput(page, appId, "backend");
173+
expect(backendTerminalOutput).toContain("Backend stdout: Hello from Flask!");
174+
expect(backendTerminalOutput).toContain("Backend stderr: This is an error message.");
175+
expect(backendTerminalOutput).not.toContain("Frontend stdout:");
176+
177+
await stopApp(page, appId);
178+
});
179+
});

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const config: PlaywrightTestConfig = {
4545
webServer: {
4646
command: `cd testing/fake-llm-server && npm run build && npm start`,
4747
url: "http://localhost:3500/health",
48+
reuseExistingServer: true, // Add this line to reuse the already running server
4849
},
4950
};
5051

src/components/chat/DyadMarkdownParser.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ function parseCustomTags(content: string): ContentPiece[] {
191191
"dyad-codebase-context",
192192
"think",
193193
"dyad-command",
194+
"run_terminal_cmd",
194195
];
195196

196197
const tagPattern = new RegExp(
@@ -424,6 +425,10 @@ function renderCustomTag(
424425
// Don't render anything for dyad-command
425426
return null;
426427

428+
case "run_terminal_cmd":
429+
// Don't render anything for run_terminal_cmd tags in chat stream
430+
return null;
431+
427432
default:
428433
return null;
429434
}

src/components/chat/DyadWrite.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
4141
const [isEditing, setIsEditing] = useState(false);
4242
const inProgress = state === "pending";
4343

44+
// Show partial content for aborted operations (if any content was received)
45+
const hasPartialContent = aborted && children && String(children).trim().length > 0;
46+
4447
const handleCancel = () => {
4548
setIsEditing(false);
4649
};
@@ -49,6 +52,9 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
4952
setIsEditing(true);
5053
setIsContentVisible(true);
5154
};
55+
56+
// Auto-show content for aborted operations with partial content
57+
const shouldAutoShowContent = hasPartialContent && !isEditing;
5258
// Extract filename from path
5359
const fileName = path ? path.split("/").pop() : "";
5460

@@ -80,7 +86,7 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
8086
{aborted && (
8187
<div className="flex items-center text-red-600 text-xs">
8288
<CircleX size={14} className="mr-1" />
83-
<span>Did not finish</span>
89+
<span>{hasPartialContent ? "Partially written" : "Did not finish"}</span>
8490
</div>
8591
)}
8692
</div>
@@ -112,9 +118,20 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
112118
Edit
113119
</button>
114120
)}
121+
{!shouldAutoShowContent && children && (
122+
<button
123+
onClick={(e) => {
124+
e.stopPropagation();
125+
setIsContentVisible(!isContentVisible);
126+
}}
127+
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 px-2 py-1 rounded cursor-pointer"
128+
>
129+
{isContentVisible ? "Hide" : "Show"} Content
130+
</button>
131+
)}
115132
</>
116133
)}
117-
{isContentVisible ? (
134+
{(isContentVisible || shouldAutoShowContent) ? (
118135
<ChevronsDownUp
119136
size={20}
120137
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
@@ -138,11 +155,16 @@ export const DyadWrite: React.FC<DyadWriteProps> = ({
138155
{description}
139156
</div>
140157
)}
141-
{isContentVisible && (
158+
{(isContentVisible || shouldAutoShowContent) && (
142159
<div
143160
className="text-xs cursor-text"
144161
onClick={(e) => e.stopPropagation()}
145162
>
163+
{hasPartialContent && !isEditing && (
164+
<div className="mb-2 p-2 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded text-orange-800 dark:text-orange-200 text-xs">
165+
⚠️ This file was partially written due to interruption. The content below may be incomplete.
166+
</div>
167+
)}
146168
{isEditing ? (
147169
<div className="h-96 min-h-96 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
148170
<FileEditor appId={appId ?? null} filePath={path} />

0 commit comments

Comments
 (0)