diff --git a/packages/api/ai/plan-parser.mts b/packages/api/ai/plan-parser.mts
index cf3ca97a..0b593f2d 100644
--- a/packages/api/ai/plan-parser.mts
+++ b/packages/api/ai/plan-parser.mts
@@ -160,3 +160,12 @@ export async function parsePlan(
throw new Error('Failed to parse XML response');
}
}
+
+export function getPackagesToInstall(plan: Plan): string[] {
+ return plan.actions
+ .filter(
+ (action): action is NpmInstallCommand =>
+ action.type === 'command' && action.command === 'npm install',
+ )
+ .flatMap((action) => action.packages);
+}
diff --git a/packages/api/apps/app.mts b/packages/api/apps/app.mts
index 626bda0c..edb707e3 100644
--- a/packages/api/apps/app.mts
+++ b/packages/api/apps/app.mts
@@ -7,7 +7,7 @@ import { asc, desc, eq } from 'drizzle-orm';
import { npmInstall } from '../exec.mjs';
import { generateApp } from '../ai/generate.mjs';
import { toValidPackageName } from '../apps/utils.mjs';
-import { parsePlan } from '../ai/plan-parser.mjs';
+import { getPackagesToInstall, parsePlan } from '../ai/plan-parser.mjs';
function toSecondsSinceEpoch(date: Date): number {
return Math.floor(date.getTime() / 1000);
@@ -54,19 +54,24 @@ export async function createAppWithAi(data: CreateAppWithAiSchemaType): Promise<
const plan = await parsePlan(result, app, data.prompt, randomid());
await applyPlan(app, plan);
- // Run npm install again since we don't have a good way of parsing the plan to know if we should...
- npmInstall({
- cwd: pathToApp(app.externalId),
- stdout(data) {
- console.log(data.toString('utf8'));
- },
- stderr(data) {
- console.error(data.toString('utf8'));
- },
- onExit(code) {
- console.log(`npm install exit code: ${code}`);
- },
- });
+ const packagesToInstall = getPackagesToInstall(plan);
+
+ if (packagesToInstall.length > 0) {
+ console.log('installing packages', packagesToInstall);
+ npmInstall({
+ cwd: pathToApp(app.externalId),
+ packages: packagesToInstall,
+ stdout(data) {
+ console.log(data.toString('utf8'));
+ },
+ stderr(data) {
+ console.error(data.toString('utf8'));
+ },
+ onExit(code) {
+ console.log(`npm install exit code: ${code}`);
+ },
+ });
+ }
return app;
}
diff --git a/packages/web/src/components/apps/header.tsx b/packages/web/src/components/apps/header.tsx
index 5535733f..f6516140 100644
--- a/packages/web/src/components/apps/header.tsx
+++ b/packages/web/src/components/apps/header.tsx
@@ -8,6 +8,7 @@ import {
CircleAlertIcon,
PanelBottomOpenIcon,
PanelBottomCloseIcon,
+ ExternalLinkIcon,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { SrcbookLogo } from '@/components/logos';
@@ -46,7 +47,7 @@ type PropsType = {
export default function EditorHeader(props: PropsType) {
const { app, updateApp } = useApp();
- const { start: startPreview, stop: stopPreview, status: previewStatus } = usePreview();
+ const { url, start: startPreview, stop: stopPreview, status: previewStatus } = usePreview();
const { status: npmInstallStatus, nodeModulesExists } = usePackageJson();
const [isExporting, setIsExporting] = useState(false);
const { open, togglePane, panelIcon } = useLogs();
@@ -170,25 +171,46 @@ export default function EditorHeader(props: PropsType) {
) : null}
{props.tab === 'preview' && previewStatus !== 'stopped' ? (
-