create-interview-cockpit 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,11 @@ import {
56
56
  updateNextjsFiles,
57
57
  updateModuleFederationFiles,
58
58
  stopNextjsSandbox,
59
+ startReactLabSandbox,
60
+ updateReactLabFiles,
61
+ streamReactLabCommand,
62
+ stopReactLabSandbox,
63
+ readReactLabFile,
59
64
  } from "../api";
60
65
  import ReactMarkdown from "react-markdown";
61
66
  import remarkGfm from "remark-gfm";
@@ -646,6 +651,15 @@ export default function CodeRunnerModal() {
646
651
  const [reactNavInput, setReactNavInput] = useState("/");
647
652
  const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
648
653
  const [reactNavIndex, setReactNavIndex] = useState(0);
654
+ // Real Vite dev-server state (React lab)
655
+ const [viteSandboxId, setViteSandboxId] = useState<string | null>(null);
656
+ const [viteSandboxUrl, setViteSandboxUrl] = useState<string | null>(null);
657
+ const [viteStarting, setViteStarting] = useState(false);
658
+ const [viteError, setViteError] = useState<string | null>(null);
659
+ const [viteConsoleCommand, setViteConsoleCommand] = useState("npm install");
660
+ const [viteConsoleOutput, setViteConsoleOutput] = useState<OutputLine[]>([]);
661
+ const [viteConsoleRunning, setViteConsoleRunning] = useState(false);
662
+ const viteIframeRef = useRef<HTMLIFrameElement>(null);
649
663
 
650
664
  // ── Sandbox output tab ("output" | "console" | "chat") ─────────────
651
665
  const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
@@ -873,6 +887,13 @@ export default function CodeRunnerModal() {
873
887
  }
874
888
  setReactPreviewSrc(null);
875
889
  setReactClientTab("edit");
890
+ if (ct === "react") {
891
+ setViteSandboxId(null);
892
+ setViteSandboxUrl(null);
893
+ setViteError(null);
894
+ setViteConsoleOutput([]);
895
+ setViteConsoleRunning(false);
896
+ }
876
897
  if (ct === "module-federation") {
877
898
  setServerCollapsed(true);
878
899
  setClientCollapsed(false);
@@ -901,7 +922,13 @@ export default function CodeRunnerModal() {
901
922
  if (sbxBottomTab === "console") {
902
923
  mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
903
924
  }
904
- }, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
925
+ }, [
926
+ mfConsoleOutput,
927
+ viteConsoleOutput,
928
+ mfConsoleRunning,
929
+ viteConsoleRunning,
930
+ sbxBottomTab,
931
+ ]);
905
932
 
906
933
  useEffect(() => {
907
934
  if (clientType !== "module-federation") return;
@@ -1278,7 +1305,11 @@ export default function CodeRunnerModal() {
1278
1305
  return files["app/page.tsx"]
1279
1306
  ? "app/page.tsx"
1280
1307
  : (Object.keys(files)[0] ?? "");
1281
- return files["App.tsx"] ? "App.tsx" : (Object.keys(files)[0] ?? "");
1308
+ return files["main.tsx"]
1309
+ ? "main.tsx"
1310
+ : files["App.tsx"]
1311
+ ? "App.tsx"
1312
+ : (Object.keys(files)[0] ?? "");
1282
1313
  };
1283
1314
 
1284
1315
  const refreshPreview = useCallback(
@@ -1325,6 +1356,15 @@ export default function CodeRunnerModal() {
1325
1356
  entry = resolved;
1326
1357
  } else {
1327
1358
  entry = getReactEntry(reactFiles, type);
1359
+ if (
1360
+ entry === "main.tsx" &&
1361
+ !/export\s+function\s+mount\s*\(/.test(reactFiles[entry] ?? "") &&
1362
+ reactFiles["App.tsx"]
1363
+ ) {
1364
+ // Compatibility path for older React labs created before main.tsx
1365
+ // exported an explicit mount() entry contract.
1366
+ entry = "App.tsx";
1367
+ }
1328
1368
  }
1329
1369
  if (!entry) return;
1330
1370
  const html = generatePreviewHTML(
@@ -1349,6 +1389,19 @@ export default function CodeRunnerModal() {
1349
1389
  }
1350
1390
  }, [reactFiles, reactClientTab, reactPreviewSrc, refreshPreview]);
1351
1391
 
1392
+ // Auto-sync file edits to the running Vite server (HMR picks them up)
1393
+ const viteFileSyncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
1394
+ useEffect(() => {
1395
+ if (!viteSandboxId) return;
1396
+ if (viteFileSyncTimer.current) clearTimeout(viteFileSyncTimer.current);
1397
+ viteFileSyncTimer.current = setTimeout(() => {
1398
+ void updateReactLabFiles(viteSandboxId, reactFiles);
1399
+ }, 400);
1400
+ return () => {
1401
+ if (viteFileSyncTimer.current) clearTimeout(viteFileSyncTimer.current);
1402
+ };
1403
+ }, [reactFiles, viteSandboxId]);
1404
+
1352
1405
  /** Navigate to a new path (updates URL bar + history + re-renders preview). */
1353
1406
  const navigatePreview = useCallback(
1354
1407
  (to: string) => {
@@ -1365,12 +1418,23 @@ export default function CodeRunnerModal() {
1365
1418
  [reactNavIndex, refreshPreview],
1366
1419
  );
1367
1420
 
1368
- // Listen for rlab-nav messages from the preview iframe
1421
+ // Listen for rlab-nav and rlab-err messages from the preview iframe
1369
1422
  useEffect(() => {
1370
1423
  const handler = (e: MessageEvent) => {
1371
1424
  if (e.data?.type === "rlab-nav" && typeof e.data.to === "string") {
1372
1425
  navigatePreview(e.data.to);
1373
1426
  }
1427
+ if (e.data?.type === "rlab-err" && typeof e.data.error === "string") {
1428
+ setOutput((prev) => [
1429
+ ...prev,
1430
+ {
1431
+ kind: "stderr",
1432
+ text: `Preview error: ${e.data.error}`,
1433
+ source: "client",
1434
+ },
1435
+ ]);
1436
+ setSbxBottomTab("output");
1437
+ }
1374
1438
  };
1375
1439
  window.addEventListener("message", handler);
1376
1440
  return () => window.removeEventListener("message", handler);
@@ -1782,16 +1846,84 @@ export default function CodeRunnerModal() {
1782
1846
  current === "console" || current === "inspector" ? "output" : current,
1783
1847
  );
1784
1848
  }
1785
- }, [clientType, nxSandboxId, mfSandboxId]);
1849
+ if (prev === "react" && clientType !== "react" && viteSandboxId) {
1850
+ void stopReactLabSandbox(viteSandboxId);
1851
+ setViteSandboxId(null);
1852
+ setViteSandboxUrl(null);
1853
+ setViteConsoleRunning(false);
1854
+ setSbxBottomTab((current) =>
1855
+ current === "console" ? "output" : current,
1856
+ );
1857
+ }
1858
+ }, [clientType, nxSandboxId, mfSandboxId, viteSandboxId]);
1786
1859
 
1787
1860
  // Clean up on unmount
1788
1861
  useEffect(() => {
1789
1862
  return () => {
1790
1863
  if (nxSandboxId) void stopNextjsSandbox(nxSandboxId);
1791
1864
  if (mfSandboxId) void stopModuleFederationSandbox(mfSandboxId);
1865
+ if (viteSandboxId) void stopReactLabSandbox(viteSandboxId);
1792
1866
  };
1793
1867
  // eslint-disable-next-line react-hooks/exhaustive-deps
1794
- }, [nxSandboxId, mfSandboxId]);
1868
+ }, [nxSandboxId, mfSandboxId, viteSandboxId]);
1869
+
1870
+ const startViteServer = useCallback(async () => {
1871
+ if (viteStarting) return;
1872
+ setViteStarting(true);
1873
+ setViteError(null);
1874
+ setViteConsoleOutput([]);
1875
+ try {
1876
+ const info = await startReactLabSandbox(reactFiles);
1877
+ setViteSandboxId(info.id);
1878
+ setViteSandboxUrl(info.url);
1879
+ setReactClientTab("preview");
1880
+ } catch (err: any) {
1881
+ setViteError(err?.message ?? String(err));
1882
+ } finally {
1883
+ setViteStarting(false);
1884
+ }
1885
+ }, [viteStarting, reactFiles]);
1886
+
1887
+ const runViteCommand = useCallback(async () => {
1888
+ if (!viteSandboxId || viteConsoleRunning) return;
1889
+ const command = viteConsoleCommand.trim();
1890
+ if (!command) return;
1891
+ setViteError(null);
1892
+ setViteConsoleRunning(true);
1893
+ setSbxBottomTab("console");
1894
+ try {
1895
+ await streamReactLabCommand({ id: viteSandboxId, command }, (message) => {
1896
+ if (message.type === "output") {
1897
+ setViteConsoleOutput((prev) => [
1898
+ ...prev,
1899
+ { kind: message.kind, text: message.text, source: "server" },
1900
+ ]);
1901
+ } else if (message.type === "error") {
1902
+ setViteConsoleOutput((prev) => [
1903
+ ...prev,
1904
+ { kind: "stderr", text: message.error, source: "server" },
1905
+ ]);
1906
+ }
1907
+ });
1908
+ // After any npm command that mutates dependencies, read package.json back
1909
+ // from disk so the editor reflects what npm actually wrote.
1910
+ const tokens = command.trim().split(/\s+/);
1911
+ const sub = tokens[1];
1912
+ if (
1913
+ sub &&
1914
+ /^(install|i|uninstall|un|remove|rm|r|update|up|upgrade|add)$/.test(sub)
1915
+ ) {
1916
+ const updated = await readReactLabFile(viteSandboxId, "package.json");
1917
+ if (updated !== null) {
1918
+ setReactFiles((prev) => ({ ...prev, "package.json": updated }));
1919
+ }
1920
+ }
1921
+ } catch (err: any) {
1922
+ setViteError(err?.message ?? String(err));
1923
+ } finally {
1924
+ setViteConsoleRunning(false);
1925
+ }
1926
+ }, [viteConsoleCommand, viteConsoleRunning, viteSandboxId]);
1795
1927
 
1796
1928
  const handleClientTypeChange = useCallback(
1797
1929
  (ct: FrontendClientType) => {
@@ -1808,6 +1940,10 @@ export default function CodeRunnerModal() {
1808
1940
  current === "console" || current === "inspector" ? "output" : current,
1809
1941
  );
1810
1942
  }
1943
+ if (ct !== "react") {
1944
+ setViteConsoleOutput([]);
1945
+ setViteConsoleRunning(false);
1946
+ }
1811
1947
  if (ct !== "script") {
1812
1948
  const defs = defaultForType(ct);
1813
1949
  setReactFiles(defs.files);
@@ -2111,6 +2247,10 @@ export default function CodeRunnerModal() {
2111
2247
  const isActiveModuleFederationGeneratedFile =
2112
2248
  clientType === "module-federation" &&
2113
2249
  moduleFederationGeneratedFileSet.has(reactActiveFile);
2250
+ const usesClientExplorer =
2251
+ clientType === "react" ||
2252
+ clientType === "nextjs" ||
2253
+ clientType === "module-federation";
2114
2254
  const mfInspectorRuntimeCount = new Set(
2115
2255
  mfInspectorEvents.map((event) => event.runtimeId),
2116
2256
  ).size;
@@ -3452,7 +3592,7 @@ export default function CodeRunnerModal() {
3452
3592
  </button>
3453
3593
  </>
3454
3594
  )}
3455
- {/* React/Next mode: optional URL + Preview button + edit/preview toggle for Next */}
3595
+ {/* React/Next/Webpack mode controls */}
3456
3596
  {(clientType === "react" ||
3457
3597
  clientType === "nextjs" ||
3458
3598
  clientType === "module-federation") && (
@@ -3465,17 +3605,69 @@ export default function CodeRunnerModal() {
3465
3605
  {sandboxUrl}
3466
3606
  </span>
3467
3607
  )}
3468
- {/* React mode: simple preview button */}
3608
+ {/* React mode: Run Vite OR edit/preview toggle */}
3469
3609
  {clientType === "react" && (
3470
- <button
3471
- type="button"
3472
- onClick={() => refreshPreview()}
3473
- className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 transition-colors shrink-0"
3474
- title="Render preview"
3475
- >
3476
- <Eye className="w-3 h-3" />
3477
- Preview
3478
- </button>
3610
+ <>
3611
+ {!viteSandboxUrl ? (
3612
+ <button
3613
+ type="button"
3614
+ onClick={() => void startViteServer()}
3615
+ disabled={viteStarting}
3616
+ className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 disabled:opacity-50 transition-colors shrink-0"
3617
+ title="Install dependencies and start Vite dev server"
3618
+ >
3619
+ {viteStarting ? (
3620
+ <Loader2 className="w-3 h-3 animate-spin" />
3621
+ ) : (
3622
+ <Play className="w-3 h-3" />
3623
+ )}
3624
+ {viteStarting ? "Starting…" : "Run Vite"}
3625
+ </button>
3626
+ ) : (
3627
+ <>
3628
+ <div className="flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
3629
+ <button
3630
+ type="button"
3631
+ onClick={() => setReactClientTab("edit")}
3632
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3633
+ reactClientTab === "edit"
3634
+ ? "bg-slate-700 text-slate-200"
3635
+ : "text-slate-500 hover:text-slate-400"
3636
+ }`}
3637
+ title="Edit code"
3638
+ >
3639
+ <Code2 className="w-2.5 h-2.5" />
3640
+ </button>
3641
+ <button
3642
+ type="button"
3643
+ onClick={() => setReactClientTab("preview")}
3644
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3645
+ reactClientTab === "preview"
3646
+ ? "bg-slate-700 text-slate-200"
3647
+ : "text-slate-500 hover:text-slate-400"
3648
+ }`}
3649
+ title="Live preview"
3650
+ >
3651
+ <Eye className="w-2.5 h-2.5" />
3652
+ </button>
3653
+ </div>
3654
+ <button
3655
+ type="button"
3656
+ onClick={() => {
3657
+ if (viteSandboxId)
3658
+ void stopReactLabSandbox(viteSandboxId);
3659
+ setViteSandboxId(null);
3660
+ setViteSandboxUrl(null);
3661
+ setReactClientTab("edit");
3662
+ }}
3663
+ className="p-0.5 rounded text-slate-600 hover:text-red-400 transition-colors shrink-0"
3664
+ title="Stop Vite lab"
3665
+ >
3666
+ <StopCircle className="w-3 h-3" />
3667
+ </button>
3668
+ </>
3669
+ )}
3670
+ </>
3479
3671
  )}
3480
3672
  {/* Next.js mode: start real server OR edit/preview toggle */}
3481
3673
  {clientType === "nextjs" && (
@@ -3596,129 +3788,12 @@ export default function CodeRunnerModal() {
3596
3788
  )}
3597
3789
  </div>
3598
3790
 
3599
- {/* File tabs row (React only — Next.js uses the tree sidebar) */}
3600
- {clientType === "react" && (
3601
- <div className="flex items-center gap-0.5 px-2 py-1 bg-slate-800/40 border-b border-slate-700 shrink-0 overflow-x-auto">
3602
- {Object.keys(reactFiles).map((fname) => (
3603
- <button
3604
- key={fname}
3605
- type="button"
3606
- onClick={() => {
3607
- setReactActiveFile(fname);
3608
- setReactClientTab("edit");
3609
- }}
3610
- className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono whitespace-nowrap transition-colors ${
3611
- fname === reactActiveFile && reactClientTab === "edit"
3612
- ? "bg-slate-900 text-slate-200 border border-slate-600"
3613
- : "text-slate-500 hover:text-slate-300 hover:bg-slate-800/50"
3614
- }`}
3615
- >
3616
- {fname.includes("/") ? fname.split("/").pop() : fname}
3617
- <span
3618
- role="button"
3619
- onClick={(e) => {
3620
- e.stopPropagation();
3621
- if (Object.keys(reactFiles).length <= 1) return;
3622
- const remaining = Object.keys(reactFiles).filter(
3623
- (f) => f !== fname,
3624
- );
3625
- setReactFiles((prev) => {
3626
- const next = { ...prev };
3627
- delete next[fname];
3628
- return next;
3629
- });
3630
- if (reactActiveFile === fname)
3631
- setReactActiveFile(remaining[0] ?? "");
3632
- }}
3633
- className="w-3 h-3 flex items-center justify-center text-slate-600 hover:text-red-400 rounded transition-colors"
3634
- title="Delete file"
3635
- >
3636
- <X className="w-2.5 h-2.5" />
3637
- </span>
3638
- </button>
3639
- ))}
3640
- {/* Add new file */}
3641
- {reactAddingFile ? (
3642
- <input
3643
- autoFocus
3644
- value={reactNewFileName}
3645
- onChange={(e) => setReactNewFileName(e.target.value)}
3646
- onBlur={() => {
3647
- setReactAddingFile(false);
3648
- setReactNewFileName("");
3649
- }}
3650
- onKeyDown={(e) => {
3651
- if (e.key === "Enter") {
3652
- e.preventDefault();
3653
- const name = reactNewFileName.trim();
3654
- if (name && !reactFiles[name]) {
3655
- setReactFiles((prev) => ({
3656
- ...prev,
3657
- [name]: newFileContent(name),
3658
- }));
3659
- setReactActiveFile(name);
3660
- setReactClientTab("edit");
3661
- }
3662
- setReactAddingFile(false);
3663
- setReactNewFileName("");
3664
- } else if (e.key === "Escape") {
3665
- setReactAddingFile(false);
3666
- setReactNewFileName("");
3667
- }
3668
- }}
3669
- placeholder="filename.tsx"
3670
- className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-200 placeholder-slate-600 outline-none focus:border-cyan-500"
3671
- />
3672
- ) : (
3673
- <button
3674
- type="button"
3675
- onClick={() => setReactAddingFile(true)}
3676
- className="p-0.5 rounded text-slate-600 hover:text-cyan-400 transition-colors shrink-0"
3677
- title="New file"
3678
- >
3679
- <FilePlus className="w-3 h-3" />
3680
- </button>
3681
- )}
3682
- {/* Edit / Preview tab toggle */}
3683
- <div className="ml-auto flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
3684
- <button
3685
- type="button"
3686
- onClick={() => setReactClientTab("edit")}
3687
- className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3688
- reactClientTab === "edit"
3689
- ? "bg-slate-700 text-slate-200"
3690
- : "text-slate-500 hover:text-slate-400"
3691
- }`}
3692
- title="Edit code"
3693
- >
3694
- <Code2 className="w-2.5 h-2.5" />
3695
- </button>
3696
- <button
3697
- type="button"
3698
- onClick={() => {
3699
- if (!reactPreviewSrc) refreshPreview();
3700
- else setReactClientTab("preview");
3701
- }}
3702
- className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3703
- reactClientTab === "preview"
3704
- ? "bg-slate-700 text-slate-200"
3705
- : "text-slate-500 hover:text-slate-400"
3706
- }`}
3707
- title="Live preview"
3708
- >
3709
- <Eye className="w-2.5 h-2.5" />
3710
- </button>
3711
- </div>
3712
- </div>
3713
- )}
3714
-
3715
3791
  {/* Client body */}
3716
3792
  <div
3717
- className={`flex-1 min-h-0 ${clientType === "nextjs" || clientType === "module-federation" ? "flex flex-row" : "relative"}`}
3793
+ className={`flex-1 min-h-0 ${usesClientExplorer ? "flex flex-row" : "relative"}`}
3718
3794
  >
3719
- {/* ── Next.js VS Code-style file tree sidebar ── */}
3720
- {(clientType === "nextjs" ||
3721
- clientType === "module-federation") && (
3795
+ {/* ── VS Code-style file tree sidebar ── */}
3796
+ {usesClientExplorer && (
3722
3797
  <div className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
3723
3798
  {/* Sidebar header */}
3724
3799
  <div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
@@ -3759,7 +3834,9 @@ export default function CodeRunnerModal() {
3759
3834
  placeholder={
3760
3835
  clientType === "module-federation"
3761
3836
  ? "apps/orders/src/App.jsx"
3762
- : "app/new.tsx"
3837
+ : clientType === "nextjs"
3838
+ ? "app/new.tsx"
3839
+ : "components/NewWidget.tsx"
3763
3840
  }
3764
3841
  className="w-full bg-slate-800 border border-cyan-600/50 rounded px-1 py-0.5 text-[9px] font-mono text-slate-200 placeholder-slate-600 outline-none focus:border-cyan-500"
3765
3842
  />
@@ -3771,7 +3848,9 @@ export default function CodeRunnerModal() {
3771
3848
  title={
3772
3849
  clientType === "module-federation"
3773
3850
  ? "New file (use paths like apps/orders/src/App.jsx)"
3774
- : "New file (use paths like app/dashboard/page.tsx)"
3851
+ : clientType === "nextjs"
3852
+ ? "New file (use paths like app/dashboard/page.tsx)"
3853
+ : "New file (use paths like components/Button.tsx)"
3775
3854
  }
3776
3855
  >
3777
3856
  <FilePlus className="w-3 h-3" />
@@ -3964,7 +4043,7 @@ export default function CodeRunnerModal() {
3964
4043
 
3965
4044
  {/* ── Editor / Preview area ── */}
3966
4045
  <div
3967
- className={`${clientType === "nextjs" || clientType === "module-federation" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
4046
+ className={`${usesClientExplorer ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
3968
4047
  >
3969
4048
  {clientType === "script" ? (
3970
4049
  <SyntaxEditor
@@ -4260,23 +4339,29 @@ export default function CodeRunnerModal() {
4260
4339
  </div>
4261
4340
  )}
4262
4341
  {((clientType === "module-federation" && mfError) ||
4263
- (clientType !== "module-federation" && nxError)) && (
4342
+ (clientType === "nextjs" && nxError) ||
4343
+ (clientType === "react" && viteError)) && (
4264
4344
  <div className="text-[10px] text-red-400 bg-red-950/40 border-b border-red-800 px-3 py-1.5 shrink-0 font-mono">
4265
4345
  {clientType === "module-federation"
4266
4346
  ? mfError
4267
- : nxError}
4347
+ : clientType === "react"
4348
+ ? viteError
4349
+ : nxError}
4268
4350
  </div>
4269
4351
  )}
4270
- {(nxStarting || mfStarting) && (
4352
+ {(nxStarting || mfStarting || viteStarting) && (
4271
4353
  <div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950">
4272
4354
  <Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
4273
4355
  <p className="text-[12px]">
4274
4356
  {clientType === "module-federation"
4275
4357
  ? "Installing dependencies and starting webpack apps…"
4276
- : "Starting Next.js dev server…"}
4358
+ : clientType === "react"
4359
+ ? "Installing dependencies and starting Vite…"
4360
+ : "Starting Next.js dev server…"}
4277
4361
  </p>
4278
4362
  <p className="text-[10px] text-slate-600 max-w-md text-center px-4">
4279
- {clientType === "module-federation"
4363
+ {clientType === "module-federation" ||
4364
+ clientType === "react"
4280
4365
  ? "The first run can take a little longer because npm install runs inside the lab sandbox."
4281
4366
  : "This takes ~10 seconds on the first run"}
4282
4367
  </p>
@@ -4342,14 +4427,47 @@ export default function CodeRunnerModal() {
4342
4427
  </p>
4343
4428
  </div>
4344
4429
  )}
4345
- {!nxStarting && clientType === "react" && (
4346
- <iframe
4347
- srcDoc={reactPreviewSrc ?? ""}
4348
- sandbox="allow-scripts"
4349
- className="flex-1 min-h-0 w-full border-0 bg-white"
4350
- title="React Preview"
4351
- />
4352
- )}
4430
+ {!viteStarting &&
4431
+ clientType === "react" &&
4432
+ viteSandboxUrl && (
4433
+ <iframe
4434
+ ref={viteIframeRef}
4435
+ src={viteSandboxUrl}
4436
+ className="flex-1 min-h-0 w-full border-0 bg-white"
4437
+ style={
4438
+ isDraggingResize
4439
+ ? { pointerEvents: "none" }
4440
+ : undefined
4441
+ }
4442
+ title="React Lab Preview"
4443
+ />
4444
+ )}
4445
+ {!viteStarting &&
4446
+ clientType === "react" &&
4447
+ !viteSandboxUrl && (
4448
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950 px-6 text-center">
4449
+ <Server className="w-8 h-8 text-cyan-400/70" />
4450
+ <p className="text-[12px]">
4451
+ Click{" "}
4452
+ <span className="text-cyan-400 font-medium">
4453
+ Run Vite
4454
+ </span>{" "}
4455
+ to install dependencies and start the dev
4456
+ server.
4457
+ </p>
4458
+ <p className="text-[10px] text-slate-600 max-w-md">
4459
+ Edit{" "}
4460
+ <code className="text-slate-400">
4461
+ package.json
4462
+ </code>{" "}
4463
+ to add packages, then use the Console tab to run{" "}
4464
+ <code className="text-slate-400">
4465
+ npm install
4466
+ </code>
4467
+ .
4468
+ </p>
4469
+ </div>
4470
+ )}
4353
4471
  </div>
4354
4472
  )}
4355
4473
  </div>
@@ -4396,7 +4514,8 @@ export default function CodeRunnerModal() {
4396
4514
  ) : null}
4397
4515
  Output
4398
4516
  </button>
4399
- {clientType === "module-federation" && (
4517
+ {(clientType === "module-federation" ||
4518
+ clientType === "react") && (
4400
4519
  <button
4401
4520
  type="button"
4402
4521
  onClick={() => setSbxBottomTab("console")}
@@ -4444,9 +4563,10 @@ export default function CodeRunnerModal() {
4444
4563
  (serverStarting || clientRunning) && (
4445
4564
  <Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
4446
4565
  )}
4447
- {sbxBottomTab === "console" && mfConsoleRunning && (
4448
- <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
4449
- )}
4566
+ {sbxBottomTab === "console" &&
4567
+ (mfConsoleRunning || viteConsoleRunning) && (
4568
+ <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
4569
+ )}
4450
4570
  {sbxBottomTab === "inspector" && mfInspectorEvents.length > 0 && (
4451
4571
  <div className="flex items-center gap-1 mr-1">
4452
4572
  <button
@@ -4495,30 +4615,39 @@ export default function CodeRunnerModal() {
4495
4615
  </button>
4496
4616
  </div>
4497
4617
  )}
4498
- {sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
4499
- <div className="flex items-center gap-1 mr-1">
4500
- <button
4501
- type="button"
4502
- onClick={() =>
4503
- navigator.clipboard.writeText(
4504
- mfConsoleOutput.map((line) => line.text).join("\n"),
4505
- )
4506
- }
4507
- className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4508
- title="Copy console output"
4509
- >
4510
- <Copy className="w-3 h-3" />
4511
- </button>
4512
- <button
4513
- type="button"
4514
- onClick={() => setMfConsoleOutput([])}
4515
- className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4516
- title="Clear console output"
4517
- >
4518
- <Trash2 className="w-3 h-3" />
4519
- </button>
4520
- </div>
4521
- )}
4618
+ {sbxBottomTab === "console" &&
4619
+ (mfConsoleOutput.length > 0 ||
4620
+ viteConsoleOutput.length > 0) && (
4621
+ <div className="flex items-center gap-1 mr-1">
4622
+ <button
4623
+ type="button"
4624
+ onClick={() => {
4625
+ const out =
4626
+ clientType === "react"
4627
+ ? viteConsoleOutput
4628
+ : mfConsoleOutput;
4629
+ navigator.clipboard.writeText(
4630
+ out.map((line) => line.text).join("\n"),
4631
+ );
4632
+ }}
4633
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4634
+ title="Copy console output"
4635
+ >
4636
+ <Copy className="w-3 h-3" />
4637
+ </button>
4638
+ <button
4639
+ type="button"
4640
+ onClick={() => {
4641
+ if (clientType === "react") setViteConsoleOutput([]);
4642
+ else setMfConsoleOutput([]);
4643
+ }}
4644
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4645
+ title="Clear console output"
4646
+ >
4647
+ <Trash2 className="w-3 h-3" />
4648
+ </button>
4649
+ </div>
4650
+ )}
4522
4651
  {sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
4523
4652
  <button
4524
4653
  type="button"
@@ -4551,7 +4680,11 @@ export default function CodeRunnerModal() {
4551
4680
  <span className="text-slate-600">
4552
4681
  {clientType === "module-federation"
4553
4682
  ? "Run webpack to start the host and remotes"
4554
- : "Start the server, then run the client"}
4683
+ : clientType === "nextjs"
4684
+ ? "Start Next.js to launch the live preview"
4685
+ : clientType === "react"
4686
+ ? "Click Run Vite to start the dev server"
4687
+ : "Start the server, then run the client"}
4555
4688
  </span>
4556
4689
  )}
4557
4690
  {sandboxOutput.map((line, i) => (
@@ -5885,90 +6018,143 @@ export default function CodeRunnerModal() {
5885
6018
  {sbxBottomTab === "console" && (
5886
6019
  <div className="flex-1 min-h-0 flex flex-col">
5887
6020
  <div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
5888
- <select
5889
- value={mfConsoleCwd}
5890
- onChange={(e) => setMfConsoleCwd(e.target.value)}
5891
- disabled={!mfSandboxId || mfConsoleRunning}
5892
- className="bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 outline-none disabled:opacity-50"
5893
- >
5894
- {moduleFederationCommandRoots.map((root) => (
5895
- <option key={root} value={root}>
5896
- {root === "." ? "root" : root}
5897
- </option>
5898
- ))}
5899
- </select>
6021
+ {clientType === "module-federation" && (
6022
+ <select
6023
+ value={mfConsoleCwd}
6024
+ onChange={(e) => setMfConsoleCwd(e.target.value)}
6025
+ disabled={!mfSandboxId || mfConsoleRunning}
6026
+ className="bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 outline-none disabled:opacity-50"
6027
+ >
6028
+ {moduleFederationCommandRoots.map((root) => (
6029
+ <option key={root} value={root}>
6030
+ {root === "." ? "root" : root}
6031
+ </option>
6032
+ ))}
6033
+ </select>
6034
+ )}
5900
6035
  <input
5901
- value={mfConsoleCommand}
5902
- onChange={(e) => setMfConsoleCommand(e.target.value)}
6036
+ value={
6037
+ clientType === "react"
6038
+ ? viteConsoleCommand
6039
+ : mfConsoleCommand
6040
+ }
6041
+ onChange={(e) =>
6042
+ clientType === "react"
6043
+ ? setViteConsoleCommand(e.target.value)
6044
+ : setMfConsoleCommand(e.target.value)
6045
+ }
5903
6046
  onKeyDown={(e) => {
5904
6047
  if (e.key === "Enter") {
5905
6048
  e.preventDefault();
5906
- void runModuleFederationCommand();
6049
+ if (clientType === "react") void runViteCommand();
6050
+ else void runModuleFederationCommand();
5907
6051
  }
5908
6052
  }}
5909
- disabled={!mfSandboxId || mfConsoleRunning}
5910
- placeholder="npm run build"
6053
+ disabled={
6054
+ clientType === "react"
6055
+ ? !viteSandboxId || viteConsoleRunning
6056
+ : !mfSandboxId || mfConsoleRunning
6057
+ }
6058
+ placeholder={
6059
+ clientType === "react"
6060
+ ? "npm install <package>"
6061
+ : "npm run build"
6062
+ }
5911
6063
  className="flex-1 bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 placeholder-slate-600 outline-none disabled:opacity-50"
5912
6064
  spellCheck={false}
5913
6065
  />
5914
6066
  <button
5915
6067
  type="button"
5916
- onClick={() => void runModuleFederationCommand()}
6068
+ onClick={() => {
6069
+ if (clientType === "react") void runViteCommand();
6070
+ else void runModuleFederationCommand();
6071
+ }}
5917
6072
  disabled={
5918
- !mfSandboxId ||
5919
- mfConsoleRunning ||
5920
- !mfConsoleCommand.trim()
6073
+ clientType === "react"
6074
+ ? !viteSandboxId ||
6075
+ viteConsoleRunning ||
6076
+ !viteConsoleCommand.trim()
6077
+ : !mfSandboxId ||
6078
+ mfConsoleRunning ||
6079
+ !mfConsoleCommand.trim()
5921
6080
  }
5922
6081
  className="flex items-center gap-1 px-2.5 py-1 rounded text-[11px] font-medium bg-cyan-600/20 hover:bg-cyan-600/35 text-cyan-300 disabled:opacity-50 transition-colors shrink-0"
5923
6082
  >
5924
- {mfConsoleRunning ? (
6083
+ {(
6084
+ clientType === "react"
6085
+ ? viteConsoleRunning
6086
+ : mfConsoleRunning
6087
+ ) ? (
5925
6088
  <Loader2 className="w-3 h-3 animate-spin" />
5926
6089
  ) : (
5927
6090
  <Play className="w-3 h-3" />
5928
6091
  )}
5929
6092
  Run
5930
6093
  </button>
5931
- <button
5932
- type="button"
5933
- onClick={() => void refreshModuleFederationGeneratedFiles()}
5934
- disabled={!mfSandboxId || mfConsoleRunning}
5935
- className="px-2 py-1 rounded text-[10px] font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-800 disabled:opacity-50 transition-colors shrink-0"
5936
- >
5937
- Refresh dist
5938
- </button>
6094
+ {clientType === "module-federation" && (
6095
+ <button
6096
+ type="button"
6097
+ onClick={() =>
6098
+ void refreshModuleFederationGeneratedFiles()
6099
+ }
6100
+ disabled={!mfSandboxId || mfConsoleRunning}
6101
+ className="px-2 py-1 rounded text-[10px] font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-800 disabled:opacity-50 transition-colors shrink-0"
6102
+ >
6103
+ Refresh dist
6104
+ </button>
6105
+ )}
5939
6106
  </div>
5940
6107
  <div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
5941
- Run npm scripts in the selected webpack app. Generated dist
5942
- files appear in the explorer as read-only artifacts.
6108
+ {clientType === "react"
6109
+ ? "Run npm commands in the React lab e.g. npm install lodash. Vite HMR reloads automatically."
6110
+ : "Run npm scripts in the selected webpack app. Generated dist files appear in the explorer as read-only artifacts."}
5943
6111
  </div>
5944
6112
  <div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
5945
- {mfConsoleOutput.length === 0 && !mfConsoleRunning && (
5946
- <span className="text-slate-600">
5947
- {mfSandboxId
5948
- ? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
5949
- : "Start webpack first, then run npm commands here."}
5950
- </span>
5951
- )}
5952
- {mfConsoleOutput.map((line, index) => (
5953
- <div key={index} className="flex items-start gap-2">
5954
- <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
5955
- cmd
5956
- </span>
5957
- <span
5958
- className={
5959
- line.kind === "stderr"
5960
- ? "text-red-400 whitespace-pre-wrap"
5961
- : line.kind === "warn"
5962
- ? "text-amber-400 whitespace-pre-wrap"
5963
- : line.kind === "info"
5964
- ? "text-slate-500 italic whitespace-pre-wrap"
5965
- : "text-slate-200 whitespace-pre-wrap"
5966
- }
5967
- >
5968
- {line.text}
5969
- </span>
5970
- </div>
5971
- ))}
6113
+ {(() => {
6114
+ const out =
6115
+ clientType === "react"
6116
+ ? viteConsoleOutput
6117
+ : mfConsoleOutput;
6118
+ const running =
6119
+ clientType === "react"
6120
+ ? viteConsoleRunning
6121
+ : mfConsoleRunning;
6122
+ const sandboxActive =
6123
+ clientType === "react" ? !!viteSandboxId : !!mfSandboxId;
6124
+ if (out.length === 0 && !running) {
6125
+ return (
6126
+ <span className="text-slate-600">
6127
+ {sandboxActive
6128
+ ? clientType === "react"
6129
+ ? "Run npm install <pkg> to add a package."
6130
+ : "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
6131
+ : clientType === "react"
6132
+ ? "Start Vite first, then run npm commands here."
6133
+ : "Start webpack first, then run npm commands here."}
6134
+ </span>
6135
+ );
6136
+ }
6137
+ return out.map((line, index) => (
6138
+ <div key={index} className="flex items-start gap-2">
6139
+ <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
6140
+ cmd
6141
+ </span>
6142
+ <span
6143
+ className={
6144
+ line.kind === "stderr"
6145
+ ? "text-red-400 whitespace-pre-wrap"
6146
+ : line.kind === "warn"
6147
+ ? "text-amber-400 whitespace-pre-wrap"
6148
+ : line.kind === "info"
6149
+ ? "text-slate-500 italic whitespace-pre-wrap"
6150
+ : "text-slate-200 whitespace-pre-wrap"
6151
+ }
6152
+ >
6153
+ {line.text}
6154
+ </span>
6155
+ </div>
6156
+ ));
6157
+ })()}
5972
6158
  <div ref={mfConsoleEndRef} />
5973
6159
  </div>
5974
6160
  </div>