Skip to content

Commit 212619d

Browse files
committed
Merge branch 'release/v0.24.0' into main
2 parents 2f149a9 + 69e228f commit 212619d

19 files changed

+2277
-621
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ Inspired by tools like *Lovable*, *v0.dev*, and *Bolt*, but with no lock-in or c
88

99
![AliFullStack Screenshot](https://github.com/user-attachments/assets/f6c83dfc-6ffd-4d32-93dd-4b9c46d17790)
1010

11-
🌐 **Live Demo & Docs**: [alifullstack.alitech.io](https://alifullstack.alitech.io)
11+
<!-- 🌐 **Live Demo & Docs**: [alifullstack.alitech.io](https://alifullstack.alitech.io) -->
1212

1313
---
1414

1515
## ⭐ Why Star This Project?
1616

1717
Help us grow! If you're excited about AI developer tools, autonomous coding, or local-first privacy-first software:
1818

19-
👉 **[Give us a ⭐ on GitHub](https://github.com/your-repo-link-here)** — it really helps!
19+
👉 **[Give us a ⭐ on GitHub](https://github.com/SFARPak/AliFullStack)** — it really helps!
2020

2121
---
2222

@@ -105,8 +105,8 @@ No sign-up. No cloud lock-in. Just download and build.
105105

106106
Be part of a growing network of **AI tinkerers**, **indie hackers**, and **full-stack dreamers**:
107107

108-
- 🧵 Reddit: [r/alifullstackbuilders](https://www.reddit.com/r/alifullstackbuilders/)
109-
- 🐦 Twitter/X: [@alifullstack](https://twitter.com/alifullstack) *(coming soon)*
108+
- 🧵 Reddit: [r/alifullstackbuilders](https://www.reddit.com/user/alifullstackbuilder/)
109+
- 🐦 Twitter/X: [@alifullstack](https://x.com/AliFullStackAI) *(coming soon)*
110110
- 🌐 Website: [alifullstack.alitech.io](https://alifullstack.alitech.io)
111111

112112
---

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+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "alifullstack",
33
"productName": "AliFullStack",
4-
"version": "0.23.0",
4+
"version": "0.24.0",
55
"description": "Free, local, open-source AI app builder",
66
"main": ".vite/build/main.js",
77
"repository": {

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/RuntimeModeSelector.tsx

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ export function RuntimeModeSelector() {
1717
return null;
1818
}
1919

20-
const isDockerMode = settings?.runtimeMode2 === "docker";
21-
20+
// Apps always run in host mode now - Docker is disabled for development
2221
const handleRuntimeModeChange = async (value: "host" | "docker") => {
22+
// Only allow host mode - Docker is disabled for app development
23+
if (value === "docker") {
24+
showError("Docker mode is disabled. Apps always run in local development mode for optimal development experience.");
25+
return;
26+
}
2327
try {
2428
await updateSettings({ runtimeMode2: value });
2529
} catch (error: any) {
@@ -43,32 +47,18 @@ export function RuntimeModeSelector() {
4347
</SelectTrigger>
4448
<SelectContent>
4549
<SelectItem value="host">Local (default)</SelectItem>
46-
<SelectItem value="docker">Docker (experimental)</SelectItem>
50+
<SelectItem value="docker" disabled>Docker (disabled)</SelectItem>
4751
</SelectContent>
4852
</Select>
4953
</div>
5054
<div className="text-sm text-gray-500 dark:text-gray-400">
51-
Choose whether to run apps directly on the local machine or in Docker
52-
containers
55+
Apps always run in local development mode for optimal development experience.
56+
Docker mode is disabled.
5357
</div>
5458
</div>
55-
{isDockerMode && (
56-
<div className="text-sm text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 p-2 rounded">
57-
⚠️ Docker mode is <b>experimental</b> and requires{" "}
58-
<button
59-
type="button"
60-
className="underline font-medium cursor-pointer"
61-
onClick={() =>
62-
IpcClient.getInstance().openExternalUrl(
63-
"https://www.docker.com/products/docker-desktop/",
64-
)
65-
}
66-
>
67-
Docker Desktop
68-
</button>{" "}
69-
to be installed and running
70-
</div>
71-
)}
59+
<div className="text-sm text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 p-2 rounded">
60+
ℹ️ Docker mode is disabled for app development. Apps run directly on your local machine for the best development experience.
61+
</div>
7262
</div>
7363
);
7464
}

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} />

src/components/preview_panel/PreviewPanel.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,24 @@ export function PreviewPanel() {
6666
messageCount > 0 ? appOutput[messageCount - 1]?.message : undefined;
6767

6868
// Auto-switch to "code" mode for backend development
69+
// Auto-switch to "code" mode for backend development if the user is not in fullstack mode
70+
// and the preview is currently in "preview" mode.
71+
// This prevents the preview from being stuck in a non-functional state when
72+
// switching to backend mode, but allows manual override.
6973
useEffect(() => {
70-
if (settings?.selectedChatMode === "backend" && previewMode === "preview") {
74+
if (
75+
settings?.selectedChatMode === "backend" &&
76+
previewMode === "preview" &&
77+
settings?.fullstackDevelopmentMode !== true
78+
) {
7179
setPreviewMode("code");
7280
}
73-
}, [settings?.selectedChatMode, previewMode, setPreviewMode]);
81+
}, [
82+
settings?.selectedChatMode,
83+
previewMode,
84+
setPreviewMode,
85+
settings?.fullstackDevelopmentMode,
86+
]);
7487

7588

7689
useEffect(() => {

0 commit comments

Comments
 (0)