create-interview-cockpit 0.7.0 → 0.9.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.
@@ -46,10 +46,13 @@ import {
46
46
  type FrontendLabType,
47
47
  } from "../reactLab";
48
48
  import {
49
+ fetchModuleFederationGeneratedFile,
50
+ fetchModuleFederationGeneratedFiles,
49
51
  fetchModuleFederationStatus,
50
52
  startModuleFederationSandbox,
51
53
  startNextjsSandbox,
52
54
  stopModuleFederationSandbox,
55
+ streamModuleFederationCommand,
53
56
  updateNextjsFiles,
54
57
  updateModuleFederationFiles,
55
58
  stopNextjsSandbox,
@@ -251,6 +254,37 @@ function buildFileTree(paths: string[]): FileTreeNode {
251
254
  return root;
252
255
  }
253
256
 
257
+ function isModuleFederationGeneratedPath(filePath: string): boolean {
258
+ return /(^|\/)dist\//.test(filePath);
259
+ }
260
+
261
+ function normalizeModuleFederationPreviewPath(input: string): string {
262
+ const trimmed = input.trim();
263
+ if (!trimmed) return "/";
264
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
265
+ }
266
+
267
+ function getModuleFederationCommandRoots(
268
+ files: Record<string, string>,
269
+ ): string[] {
270
+ const roots = new Set<string>(["."]);
271
+
272
+ for (const filePath of Object.keys(files)) {
273
+ const match = filePath.match(/^(apps\/[^/]+)/);
274
+ if (match) {
275
+ roots.add(match[1]);
276
+ }
277
+ }
278
+
279
+ return Array.from(roots).sort((a, b) => {
280
+ if (a === ".") return -1;
281
+ if (b === ".") return 1;
282
+ return a.localeCompare(b);
283
+ });
284
+ }
285
+
286
+ type SbxBottomTab = "output" | "console" | "chat";
287
+
254
288
  export default function CodeRunnerModal() {
255
289
  const {
256
290
  closeCodeRunner,
@@ -314,20 +348,32 @@ export default function CodeRunnerModal() {
314
348
  const [nxStarting, setNxStarting] = useState(false);
315
349
  const [nxError, setNxError] = useState<string | null>(null);
316
350
  const nxIframeRef = useRef<HTMLIFrameElement>(null);
351
+ const mfIframeRef = useRef<HTMLIFrameElement>(null);
317
352
  const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
318
353
  const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
319
354
  const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
320
355
  const [mfStarting, setMfStarting] = useState(false);
321
356
  const [mfError, setMfError] = useState<string | null>(null);
322
357
  const [mfPreviewApp, setMfPreviewApp] = useState("host");
358
+ const [mfPreviewPath, setMfPreviewPath] = useState("/");
359
+ const [mfNavInput, setMfNavInput] = useState("/");
360
+ const [mfConsoleOutput, setMfConsoleOutput] = useState<OutputLine[]>([]);
361
+ const [mfConsoleCommand, setMfConsoleCommand] = useState("npm run build");
362
+ const [mfConsoleCwd, setMfConsoleCwd] = useState("apps/host");
363
+ const [mfConsoleRunning, setMfConsoleRunning] = useState(false);
364
+ const [mfGeneratedFiles, setMfGeneratedFiles] = useState<string[]>([]);
365
+ const [mfGeneratedFileContents, setMfGeneratedFileContents] = useState<
366
+ Record<string, string>
367
+ >({});
368
+ const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
323
369
  // Simulated URL bar state for Next.js mode
324
370
  const [reactPreviewPath, setReactPreviewPath] = useState("/");
325
371
  const [reactNavInput, setReactNavInput] = useState("/");
326
372
  const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
327
373
  const [reactNavIndex, setReactNavIndex] = useState(0);
328
374
 
329
- // ── Sandbox output tab ("output" | "chat") ──────────────────
330
- const [sbxBottomTab, setSbxBottomTab] = useState<"output" | "chat">("output");
375
+ // ── Sandbox output tab ("output" | "console" | "chat") ─────────────
376
+ const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
331
377
 
332
378
  // ── Sandbox panel sizes ─────────────────────────────────────────
333
379
  // sbxSplit: server pane width as % of the editor row (0–100)
@@ -499,7 +545,9 @@ export default function CodeRunnerModal() {
499
545
  };
500
546
 
501
547
  const outputEndRef = useRef<HTMLDivElement>(null);
548
+ const mfConsoleEndRef = useRef<HTMLDivElement>(null);
502
549
  const nameInputRef = useRef<HTMLInputElement>(null);
550
+ const mfGeneratedFileRequestRef = useRef<string | null>(null);
503
551
  // Tracks how many server log lines have already been flushed to sandboxOutput
504
552
  const sandboxLogOffsetRef = useRef(0);
505
553
  // Stable ref so unmount cleanup can stop sandbox without stale closure
@@ -553,6 +601,14 @@ export default function CodeRunnerModal() {
553
601
  setServerCollapsed(true);
554
602
  setClientCollapsed(false);
555
603
  setMfPreviewApp("host");
604
+ setMfPreviewPath("/");
605
+ setMfNavInput("/");
606
+ setMfConsoleCommand("npm run build");
607
+ setMfConsoleCwd("apps/host");
608
+ setMfConsoleOutput([]);
609
+ setMfGeneratedFiles([]);
610
+ setMfGeneratedFileContents({});
611
+ setMfLoadingFile(null);
556
612
  }
557
613
  }
558
614
  }, [runnerInitialSandbox]);
@@ -564,6 +620,20 @@ export default function CodeRunnerModal() {
564
620
  outputEndRef.current?.scrollIntoView({ behavior: "smooth" });
565
621
  }, [output]);
566
622
 
623
+ useEffect(() => {
624
+ if (sbxBottomTab === "console") {
625
+ mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
626
+ }
627
+ }, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
628
+
629
+ useEffect(() => {
630
+ if (clientType !== "module-federation") return;
631
+ const roots = getModuleFederationCommandRoots(reactFiles);
632
+ if (!roots.includes(mfConsoleCwd)) {
633
+ setMfConsoleCwd(roots.find((root) => root !== ".") ?? ".");
634
+ }
635
+ }, [clientType, reactFiles, mfConsoleCwd]);
636
+
567
637
  // ── Sandbox divider drag handlers ────────────────────────────────
568
638
  useEffect(() => {
569
639
  const onMove = (e: MouseEvent) => {
@@ -1038,10 +1108,51 @@ export default function CodeRunnerModal() {
1038
1108
  }
1039
1109
  }, [nxStarting, reactFiles]);
1040
1110
 
1111
+ const refreshModuleFederationGeneratedFiles = useCallback(
1112
+ async (sandboxId: string | null = mfSandboxId): Promise<string[]> => {
1113
+ if (!sandboxId) {
1114
+ setMfGeneratedFiles([]);
1115
+ setMfGeneratedFileContents({});
1116
+ setMfLoadingFile(null);
1117
+ return [];
1118
+ }
1119
+
1120
+ try {
1121
+ const files = await fetchModuleFederationGeneratedFiles(sandboxId);
1122
+ setMfGeneratedFiles(files);
1123
+ setMfGeneratedFileContents((prev) =>
1124
+ Object.fromEntries(
1125
+ Object.entries(prev).filter(([filePath]) =>
1126
+ files.includes(filePath),
1127
+ ),
1128
+ ),
1129
+ );
1130
+
1131
+ if (
1132
+ reactActiveFile &&
1133
+ !Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile) &&
1134
+ !files.includes(reactActiveFile)
1135
+ ) {
1136
+ setReactActiveFile(Object.keys(reactFiles)[0] ?? files[0] ?? "");
1137
+ }
1138
+
1139
+ return files;
1140
+ } catch (err: any) {
1141
+ setMfError(err?.message ?? String(err));
1142
+ return [];
1143
+ }
1144
+ },
1145
+ [mfSandboxId, reactActiveFile, reactFiles],
1146
+ );
1147
+
1041
1148
  const startModuleFederationServer = useCallback(async () => {
1042
1149
  if (mfStarting) return;
1043
1150
  setMfStarting(true);
1044
1151
  setMfError(null);
1152
+ setMfGeneratedFiles([]);
1153
+ setMfGeneratedFileContents({});
1154
+ setMfLoadingFile(null);
1155
+ setMfConsoleOutput([]);
1045
1156
  setSbxBottomTab("output");
1046
1157
  setSandboxOutput([
1047
1158
  {
@@ -1058,9 +1169,12 @@ export default function CodeRunnerModal() {
1058
1169
  setMfPreviewApp(
1059
1170
  info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
1060
1171
  );
1172
+ setMfPreviewPath("/");
1173
+ setMfNavInput("/");
1061
1174
  setReactClientTab("preview");
1062
1175
  setServerCollapsed(true);
1063
1176
  setClientCollapsed(false);
1177
+ void refreshModuleFederationGeneratedFiles(info.id);
1064
1178
  setSandboxOutput((prev) => [
1065
1179
  ...prev,
1066
1180
  {
@@ -1079,7 +1193,7 @@ export default function CodeRunnerModal() {
1079
1193
  } finally {
1080
1194
  setMfStarting(false);
1081
1195
  }
1082
- }, [mfStarting, reactFiles]);
1196
+ }, [mfStarting, reactFiles, refreshModuleFederationGeneratedFiles]);
1083
1197
 
1084
1198
  const stopModuleFederationServer = useCallback(async () => {
1085
1199
  if (!mfSandboxId) return;
@@ -1088,6 +1202,10 @@ export default function CodeRunnerModal() {
1088
1202
  setMfHostUrl(null);
1089
1203
  setMfAppUrls({});
1090
1204
  setMfError(null);
1205
+ setMfGeneratedFiles([]);
1206
+ setMfGeneratedFileContents({});
1207
+ setMfLoadingFile(null);
1208
+ setMfConsoleRunning(false);
1091
1209
  setSandboxOutput((prev) => [
1092
1210
  ...prev,
1093
1211
  {
@@ -1148,6 +1266,10 @@ export default function CodeRunnerModal() {
1148
1266
  setMfHostUrl(null);
1149
1267
  setMfAppUrls({});
1150
1268
  setMfError(null);
1269
+ setMfGeneratedFiles([]);
1270
+ setMfGeneratedFileContents({});
1271
+ setMfLoadingFile(null);
1272
+ setMfConsoleRunning(false);
1151
1273
  return;
1152
1274
  }
1153
1275
  if (status.hostUrl) setMfHostUrl(status.hostUrl);
@@ -1180,6 +1302,143 @@ export default function CodeRunnerModal() {
1180
1302
  return () => clearInterval(interval);
1181
1303
  }, [mfSandboxId]);
1182
1304
 
1305
+ useEffect(() => {
1306
+ if (clientType !== "module-federation" || !mfSandboxId) return;
1307
+ if (!reactActiveFile) return;
1308
+ if (Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile)) {
1309
+ return;
1310
+ }
1311
+ if (!mfGeneratedFiles.includes(reactActiveFile)) return;
1312
+ if (mfGeneratedFileContents[reactActiveFile] !== undefined) return;
1313
+ if (mfGeneratedFileRequestRef.current === reactActiveFile) return;
1314
+
1315
+ const filePath = reactActiveFile;
1316
+ let cancelled = false;
1317
+ mfGeneratedFileRequestRef.current = filePath;
1318
+ setMfLoadingFile(filePath);
1319
+
1320
+ void fetchModuleFederationGeneratedFile(mfSandboxId, filePath)
1321
+ .then(({ content }) => {
1322
+ if (cancelled) return;
1323
+ setMfGeneratedFileContents((prev) => ({
1324
+ ...prev,
1325
+ [filePath]: content,
1326
+ }));
1327
+ })
1328
+ .catch((error: any) => {
1329
+ if (cancelled) return;
1330
+ const message = error?.message ?? String(error);
1331
+ setMfError(message);
1332
+ setMfGeneratedFileContents((prev) => ({
1333
+ ...prev,
1334
+ [filePath]: `Failed to load generated file.\n\n${message}`,
1335
+ }));
1336
+ })
1337
+ .finally(() => {
1338
+ if (mfGeneratedFileRequestRef.current === filePath) {
1339
+ mfGeneratedFileRequestRef.current = null;
1340
+ }
1341
+ if (cancelled) return;
1342
+ setMfLoadingFile((current) => (current === filePath ? null : current));
1343
+ });
1344
+
1345
+ return () => {
1346
+ cancelled = true;
1347
+ };
1348
+ }, [
1349
+ clientType,
1350
+ mfGeneratedFileContents,
1351
+ mfGeneratedFiles,
1352
+ mfSandboxId,
1353
+ reactActiveFile,
1354
+ reactFiles,
1355
+ ]);
1356
+
1357
+ const runModuleFederationCommand = useCallback(async () => {
1358
+ if (!mfSandboxId || mfConsoleRunning) return;
1359
+ const command = mfConsoleCommand.trim();
1360
+ if (!command) return;
1361
+
1362
+ setMfError(null);
1363
+ setMfConsoleRunning(true);
1364
+ setSbxBottomTab("console");
1365
+
1366
+ let streamError: string | null = null;
1367
+
1368
+ try {
1369
+ await streamModuleFederationCommand(
1370
+ {
1371
+ id: mfSandboxId,
1372
+ command,
1373
+ cwd: mfConsoleCwd === "." ? undefined : mfConsoleCwd,
1374
+ },
1375
+ (message) => {
1376
+ if (message.type === "output") {
1377
+ setMfConsoleOutput((prev) => [
1378
+ ...prev,
1379
+ {
1380
+ kind: message.kind,
1381
+ text: message.text,
1382
+ source: "server",
1383
+ },
1384
+ ]);
1385
+ return;
1386
+ }
1387
+
1388
+ if (message.type === "error") {
1389
+ streamError = message.error;
1390
+ setMfConsoleOutput((prev) => [
1391
+ ...prev,
1392
+ {
1393
+ kind: "stderr",
1394
+ text: message.error,
1395
+ source: "server",
1396
+ },
1397
+ ]);
1398
+ }
1399
+ },
1400
+ );
1401
+
1402
+ if (streamError) {
1403
+ throw new Error(streamError);
1404
+ }
1405
+
1406
+ const files = await refreshModuleFederationGeneratedFiles(mfSandboxId);
1407
+ setMfConsoleOutput((prev) => [
1408
+ ...prev,
1409
+ {
1410
+ kind: "info",
1411
+ text:
1412
+ files.length > 0
1413
+ ? `Refreshed ${files.length} generated file${files.length === 1 ? "" : "s"}.`
1414
+ : "No generated dist files were found.",
1415
+ source: "server",
1416
+ },
1417
+ ]);
1418
+ } catch (err: any) {
1419
+ const message = err?.message ?? String(err);
1420
+ setMfError(message);
1421
+ if (streamError !== message) {
1422
+ setMfConsoleOutput((prev) => [
1423
+ ...prev,
1424
+ {
1425
+ kind: "stderr",
1426
+ text: message,
1427
+ source: "server",
1428
+ },
1429
+ ]);
1430
+ }
1431
+ } finally {
1432
+ setMfConsoleRunning(false);
1433
+ }
1434
+ }, [
1435
+ mfConsoleCommand,
1436
+ mfConsoleCwd,
1437
+ mfConsoleRunning,
1438
+ mfSandboxId,
1439
+ refreshModuleFederationGeneratedFiles,
1440
+ ]);
1441
+
1183
1442
  // Clean up Next.js server when the modal is closed or mode changes away from nextjs
1184
1443
  const prevClientTypeRef = useRef(clientType);
1185
1444
  useEffect(() => {
@@ -1199,6 +1458,13 @@ export default function CodeRunnerModal() {
1199
1458
  setMfSandboxId(null);
1200
1459
  setMfHostUrl(null);
1201
1460
  setMfAppUrls({});
1461
+ setMfGeneratedFiles([]);
1462
+ setMfGeneratedFileContents({});
1463
+ setMfLoadingFile(null);
1464
+ setMfConsoleRunning(false);
1465
+ setSbxBottomTab((current) =>
1466
+ current === "console" ? "output" : current,
1467
+ );
1202
1468
  }
1203
1469
  }, [clientType, nxSandboxId, mfSandboxId]);
1204
1470
 
@@ -1215,6 +1481,16 @@ export default function CodeRunnerModal() {
1215
1481
  (ct: FrontendClientType) => {
1216
1482
  if (ct === clientType) return;
1217
1483
  setClientType(ct);
1484
+ if (ct !== "module-federation") {
1485
+ setMfGeneratedFiles([]);
1486
+ setMfGeneratedFileContents({});
1487
+ setMfLoadingFile(null);
1488
+ setMfConsoleOutput([]);
1489
+ setMfConsoleRunning(false);
1490
+ setSbxBottomTab((current) =>
1491
+ current === "console" ? "output" : current,
1492
+ );
1493
+ }
1218
1494
  if (ct !== "script") {
1219
1495
  const defs = defaultForType(ct);
1220
1496
  setReactFiles(defs.files);
@@ -1231,8 +1507,14 @@ export default function CodeRunnerModal() {
1231
1507
  setServerCollapsed(true);
1232
1508
  setClientCollapsed(false);
1233
1509
  setMfPreviewApp("host");
1510
+ setMfPreviewPath("/");
1511
+ setMfNavInput("/");
1234
1512
  setMfError(null);
1235
1513
  }
1514
+ if (ct === "module-federation") {
1515
+ setMfConsoleCommand("npm run build");
1516
+ setMfConsoleCwd("apps/host");
1517
+ }
1236
1518
  }
1237
1519
  },
1238
1520
  [clientType],
@@ -1482,6 +1764,37 @@ export default function CodeRunnerModal() {
1482
1764
  minHeight: MIN_H,
1483
1765
  };
1484
1766
 
1767
+ const moduleFederationCommandRoots =
1768
+ getModuleFederationCommandRoots(reactFiles);
1769
+ const moduleFederationGeneratedFiles = mfGeneratedFiles.filter(
1770
+ (filePath) => !Object.prototype.hasOwnProperty.call(reactFiles, filePath),
1771
+ );
1772
+ const moduleFederationGeneratedFileSet = new Set(
1773
+ moduleFederationGeneratedFiles,
1774
+ );
1775
+ const moduleFederationPreviewBaseUrl =
1776
+ mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? "";
1777
+ const moduleFederationPreviewPath =
1778
+ normalizeModuleFederationPreviewPath(mfPreviewPath);
1779
+ const moduleFederationPreviewUrl = moduleFederationPreviewBaseUrl
1780
+ ? `${moduleFederationPreviewBaseUrl}${moduleFederationPreviewPath}`
1781
+ : "";
1782
+ const moduleFederationPreviewHostLabel = moduleFederationPreviewBaseUrl
1783
+ ? moduleFederationPreviewBaseUrl.replace(/^https?:\/\//, "")
1784
+ : "localhost";
1785
+ const visibleReactFiles =
1786
+ clientType === "module-federation"
1787
+ ? Array.from(
1788
+ new Set([
1789
+ ...Object.keys(reactFiles),
1790
+ ...moduleFederationGeneratedFiles,
1791
+ ]),
1792
+ )
1793
+ : Object.keys(reactFiles);
1794
+ const isActiveModuleFederationGeneratedFile =
1795
+ clientType === "module-federation" &&
1796
+ moduleFederationGeneratedFileSet.has(reactActiveFile);
1797
+
1485
1798
  return (
1486
1799
  <div
1487
1800
  className="fixed z-[60] flex flex-col bg-slate-900 border border-slate-700 rounded-xl shadow-2xl overflow-hidden select-none"
@@ -2564,9 +2877,18 @@ export default function CodeRunnerModal() {
2564
2877
  {/* Tree nodes */}
2565
2878
  <div className="flex-1 py-1">
2566
2879
  {(() => {
2567
- const tree = buildFileTree(Object.keys(reactFiles));
2880
+ const tree = buildFileTree(visibleReactFiles);
2568
2881
 
2569
- const fileIcon = (name: string) => {
2882
+ const fileIcon = (
2883
+ name: string,
2884
+ isGenerated = false,
2885
+ ) => {
2886
+ if (isGenerated)
2887
+ return (
2888
+ <span className="text-emerald-400 mr-1 text-[8px] font-semibold uppercase">
2889
+ dist
2890
+ </span>
2891
+ );
2570
2892
  if (name.endsWith(".tsx") || name.endsWith(".jsx"))
2571
2893
  return (
2572
2894
  <span className="text-cyan-400 mr-1 text-[9px]">
@@ -2588,47 +2910,74 @@ export default function CodeRunnerModal() {
2588
2910
 
2589
2911
  const renderFile = (path: string, indent = 0) => (
2590
2912
  <div key={path} className="group flex items-center">
2591
- <button
2592
- type="button"
2593
- onClick={() => {
2594
- setReactActiveFile(path);
2595
- setReactClientTab("edit");
2596
- }}
2597
- style={{ paddingLeft: `${8 + indent * 10}px` }}
2598
- className={`flex-1 flex items-center gap-0.5 py-0.5 pr-1 text-left text-[10px] font-mono truncate transition-colors ${
2599
- path === reactActiveFile &&
2600
- reactClientTab === "edit"
2601
- ? "bg-slate-700 text-slate-100"
2602
- : "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
2603
- }`}
2604
- title={path}
2605
- >
2606
- {fileIcon(path.split("/").pop() ?? path)}
2607
- <span className="truncate">
2608
- {path.split("/").pop()}
2609
- </span>
2610
- </button>
2611
- <button
2612
- type="button"
2613
- onClick={() => {
2614
- if (Object.keys(reactFiles).length <= 1)
2615
- return;
2616
- const remaining = Object.keys(
2617
- reactFiles,
2618
- ).filter((f) => f !== path);
2619
- setReactFiles((prev) => {
2620
- const next = { ...prev };
2621
- delete next[path];
2622
- return next;
2623
- });
2624
- if (reactActiveFile === path)
2625
- setReactActiveFile(remaining[0] ?? "");
2626
- }}
2627
- className="opacity-0 group-hover:opacity-100 p-0.5 mr-1 rounded text-slate-600 hover:text-red-400 transition-all shrink-0"
2628
- title="Delete file"
2629
- >
2630
- <X className="w-2.5 h-2.5" />
2631
- </button>
2913
+ {(() => {
2914
+ const isGenerated =
2915
+ moduleFederationGeneratedFileSet.has(path);
2916
+ return (
2917
+ <>
2918
+ <button
2919
+ type="button"
2920
+ onClick={() => {
2921
+ setReactActiveFile(path);
2922
+ setReactClientTab("edit");
2923
+ }}
2924
+ style={{
2925
+ paddingLeft: `${8 + indent * 10}px`,
2926
+ }}
2927
+ className={`flex-1 flex items-center gap-0.5 py-0.5 pr-1 text-left text-[10px] font-mono truncate transition-colors ${
2928
+ path === reactActiveFile &&
2929
+ reactClientTab === "edit"
2930
+ ? isGenerated
2931
+ ? "bg-slate-700 text-emerald-100"
2932
+ : "bg-slate-700 text-slate-100"
2933
+ : isGenerated
2934
+ ? "text-emerald-300/80 hover:bg-slate-800 hover:text-emerald-100"
2935
+ : "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
2936
+ }`}
2937
+ title={
2938
+ isGenerated
2939
+ ? `${path} (generated)`
2940
+ : path
2941
+ }
2942
+ >
2943
+ {fileIcon(
2944
+ path.split("/").pop() ?? path,
2945
+ isGenerated,
2946
+ )}
2947
+ <span className="truncate">
2948
+ {path.split("/").pop()}
2949
+ </span>
2950
+ </button>
2951
+ {!isGenerated && (
2952
+ <button
2953
+ type="button"
2954
+ onClick={() => {
2955
+ if (
2956
+ Object.keys(reactFiles).length <= 1
2957
+ )
2958
+ return;
2959
+ const remaining = Object.keys(
2960
+ reactFiles,
2961
+ ).filter((f) => f !== path);
2962
+ setReactFiles((prev) => {
2963
+ const next = { ...prev };
2964
+ delete next[path];
2965
+ return next;
2966
+ });
2967
+ if (reactActiveFile === path)
2968
+ setReactActiveFile(
2969
+ remaining[0] ?? "",
2970
+ );
2971
+ }}
2972
+ className="opacity-0 group-hover:opacity-100 p-0.5 mr-1 rounded text-slate-600 hover:text-red-400 transition-all shrink-0"
2973
+ title="Delete file"
2974
+ >
2975
+ <X className="w-2.5 h-2.5" />
2976
+ </button>
2977
+ )}
2978
+ </>
2979
+ );
2980
+ })()}
2632
2981
  </div>
2633
2982
  );
2634
2983
 
@@ -2728,25 +3077,48 @@ export default function CodeRunnerModal() {
2728
3077
  }
2729
3078
  />
2730
3079
  ) : reactClientTab === "edit" ? (
2731
- <SyntaxEditor
2732
- key={reactActiveFile}
2733
- value={reactFiles[reactActiveFile] ?? ""}
2734
- onChange={(val) =>
2735
- setReactFiles((prev) => ({
2736
- ...prev,
2737
- [reactActiveFile]: val,
2738
- }))
2739
- }
2740
- language={
2741
- reactActiveFile.endsWith(".ts") ||
2742
- reactActiveFile.endsWith(".tsx")
2743
- ? "typescript"
2744
- : "javascript"
2745
- }
2746
- fontSize="12px"
2747
- focusRingClass="ring-cyan-500/30"
2748
- placeholder={`// ${reactActiveFile}\n`}
2749
- />
3080
+ isActiveModuleFederationGeneratedFile ? (
3081
+ <div className="absolute inset-0 flex flex-col bg-slate-950">
3082
+ <div className="shrink-0 border-b border-slate-800 bg-slate-900/80 px-3 py-1.5 text-[10px] font-mono text-emerald-300">
3083
+ Read-only build artifact from dist/. Run the webpack
3084
+ console again to refresh it.
3085
+ </div>
3086
+ <div className="flex-1 overflow-auto px-3 py-3 font-mono text-[12px] leading-relaxed text-slate-200">
3087
+ {mfLoadingFile === reactActiveFile &&
3088
+ mfGeneratedFileContents[reactActiveFile] ===
3089
+ undefined ? (
3090
+ <div className="flex h-full items-center justify-center gap-2 text-slate-500">
3091
+ <Loader2 className="w-4 h-4 animate-spin" />
3092
+ <span>Loading generated file…</span>
3093
+ </div>
3094
+ ) : (
3095
+ <pre className="m-0 min-w-max whitespace-pre">
3096
+ {mfGeneratedFileContents[reactActiveFile] ?? ""}
3097
+ </pre>
3098
+ )}
3099
+ </div>
3100
+ </div>
3101
+ ) : (
3102
+ <SyntaxEditor
3103
+ key={reactActiveFile}
3104
+ value={reactFiles[reactActiveFile] ?? ""}
3105
+ onChange={(val) =>
3106
+ setReactFiles((prev) => ({
3107
+ ...prev,
3108
+ [reactActiveFile]: val,
3109
+ }))
3110
+ }
3111
+ language={
3112
+ reactActiveFile.endsWith(".ts") ||
3113
+ reactActiveFile.endsWith(".tsx")
3114
+ ? "typescript"
3115
+ : "javascript"
3116
+ }
3117
+ fontSize="12px"
3118
+ focusRingClass="ring-cyan-500/30"
3119
+ placeholder={`// ${reactActiveFile}\n`}
3120
+ />
3121
+ )
2750
3122
  ) : (
2751
3123
  <div className="w-full h-full flex flex-col">
2752
3124
  {clientType === "nextjs" && (
@@ -2883,7 +3255,7 @@ export default function CodeRunnerModal() {
2883
3255
  </div>
2884
3256
  )}
2885
3257
  {clientType === "module-federation" && (
2886
- <div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 overflow-x-auto">
3258
+ <div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 min-w-0">
2887
3259
  {Object.entries(mfAppUrls).map(([name, url]) => (
2888
3260
  <button
2889
3261
  key={name}
@@ -2899,11 +3271,69 @@ export default function CodeRunnerModal() {
2899
3271
  {name}
2900
3272
  </button>
2901
3273
  ))}
2902
- <div className="ml-auto text-[9px] font-mono text-slate-600 shrink-0">
2903
- {mfAppUrls[mfPreviewApp] ??
2904
- mfHostUrl ??
2905
- "Start webpack to preview"}
2906
- </div>
3274
+ <button
3275
+ type="button"
3276
+ onClick={() => {
3277
+ if (
3278
+ mfIframeRef.current &&
3279
+ moduleFederationPreviewUrl
3280
+ ) {
3281
+ mfIframeRef.current.src =
3282
+ moduleFederationPreviewUrl;
3283
+ }
3284
+ }}
3285
+ className="p-0.5 rounded text-slate-500 hover:text-slate-200 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shrink-0"
3286
+ title="Refresh"
3287
+ disabled={!moduleFederationPreviewUrl}
3288
+ >
3289
+ <svg
3290
+ className="w-3 h-3"
3291
+ viewBox="0 0 16 16"
3292
+ fill="currentColor"
3293
+ >
3294
+ <path d="M13.65 2.35A8 8 0 1 0 15 8h-2a6 6 0 1 1-1.1-3.48L10 6h5V1l-1.35 1.35z" />
3295
+ </svg>
3296
+ </button>
3297
+ <form
3298
+ className="flex-1 flex items-center gap-1 bg-slate-900 border border-slate-600 rounded px-2 py-0.5 focus-within:border-cyan-500/60 transition-colors min-w-0"
3299
+ onSubmit={(e) => {
3300
+ e.preventDefault();
3301
+ const nextPath =
3302
+ normalizeModuleFederationPreviewPath(
3303
+ mfNavInput,
3304
+ );
3305
+ setMfPreviewPath(nextPath);
3306
+ setMfNavInput(nextPath);
3307
+ if (
3308
+ nextPath === moduleFederationPreviewPath &&
3309
+ mfIframeRef.current &&
3310
+ moduleFederationPreviewBaseUrl
3311
+ ) {
3312
+ mfIframeRef.current.src = `${moduleFederationPreviewBaseUrl}${nextPath}`;
3313
+ }
3314
+ }}
3315
+ >
3316
+ <span className="text-slate-600 text-[9px] font-mono select-none shrink-0">
3317
+ {moduleFederationPreviewHostLabel}
3318
+ </span>
3319
+ <input
3320
+ value={mfNavInput}
3321
+ onChange={(e) => setMfNavInput(e.target.value)}
3322
+ onFocus={(e) => e.target.select()}
3323
+ className="flex-1 bg-transparent text-[11px] font-mono text-slate-200 outline-none placeholder-slate-600 min-w-0"
3324
+ placeholder="/"
3325
+ spellCheck={false}
3326
+ />
3327
+ </form>
3328
+ {moduleFederationPreviewUrl ? (
3329
+ <span className="text-[9px] font-mono text-green-400 shrink-0">
3330
+ ● live
3331
+ </span>
3332
+ ) : (
3333
+ <span className="text-[9px] font-mono text-slate-600 shrink-0">
3334
+ Start webpack to preview
3335
+ </span>
3336
+ )}
2907
3337
  </div>
2908
3338
  )}
2909
3339
  {((clientType === "module-federation" && mfError) ||
@@ -2955,9 +3385,10 @@ export default function CodeRunnerModal() {
2955
3385
  {!mfStarting &&
2956
3386
  clientType === "module-federation" &&
2957
3387
  mfSandboxId &&
2958
- (mfAppUrls[mfPreviewApp] ?? mfHostUrl) && (
3388
+ moduleFederationPreviewUrl && (
2959
3389
  <iframe
2960
- src={mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? ""}
3390
+ ref={mfIframeRef}
3391
+ src={moduleFederationPreviewUrl}
2961
3392
  className="flex-1 min-h-0 w-full border-0 bg-white"
2962
3393
  title="Webpack Module Federation Preview"
2963
3394
  />
@@ -3029,6 +3460,20 @@ export default function CodeRunnerModal() {
3029
3460
  ) : null}
3030
3461
  Output
3031
3462
  </button>
3463
+ {clientType === "module-federation" && (
3464
+ <button
3465
+ type="button"
3466
+ onClick={() => setSbxBottomTab("console")}
3467
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
3468
+ sbxBottomTab === "console"
3469
+ ? "border-cyan-500 text-cyan-300"
3470
+ : "border-transparent text-slate-500 hover:text-slate-300"
3471
+ }`}
3472
+ >
3473
+ <Terminal className="w-3 h-3" />
3474
+ Console
3475
+ </button>
3476
+ )}
3032
3477
  <button
3033
3478
  type="button"
3034
3479
  onClick={() => {
@@ -3049,6 +3494,9 @@ export default function CodeRunnerModal() {
3049
3494
  (serverStarting || clientRunning) && (
3050
3495
  <Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
3051
3496
  )}
3497
+ {sbxBottomTab === "console" && mfConsoleRunning && (
3498
+ <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
3499
+ )}
3052
3500
  {sbxBottomTab === "output" && sandboxOutput.length > 0 && (
3053
3501
  <div className="flex items-center gap-1 mr-1">
3054
3502
  <button
@@ -3073,6 +3521,30 @@ export default function CodeRunnerModal() {
3073
3521
  </button>
3074
3522
  </div>
3075
3523
  )}
3524
+ {sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
3525
+ <div className="flex items-center gap-1 mr-1">
3526
+ <button
3527
+ type="button"
3528
+ onClick={() =>
3529
+ navigator.clipboard.writeText(
3530
+ mfConsoleOutput.map((line) => line.text).join("\n"),
3531
+ )
3532
+ }
3533
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
3534
+ title="Copy console output"
3535
+ >
3536
+ <Copy className="w-3 h-3" />
3537
+ </button>
3538
+ <button
3539
+ type="button"
3540
+ onClick={() => setMfConsoleOutput([])}
3541
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
3542
+ title="Clear console output"
3543
+ >
3544
+ <Trash2 className="w-3 h-3" />
3545
+ </button>
3546
+ </div>
3547
+ )}
3076
3548
  {sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
3077
3549
  <button
3078
3550
  type="button"
@@ -3144,6 +3616,98 @@ export default function CodeRunnerModal() {
3144
3616
  </div>
3145
3617
  )}
3146
3618
 
3619
+ {sbxBottomTab === "console" && (
3620
+ <div className="flex-1 min-h-0 flex flex-col">
3621
+ <div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
3622
+ <select
3623
+ value={mfConsoleCwd}
3624
+ onChange={(e) => setMfConsoleCwd(e.target.value)}
3625
+ disabled={!mfSandboxId || mfConsoleRunning}
3626
+ 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"
3627
+ >
3628
+ {moduleFederationCommandRoots.map((root) => (
3629
+ <option key={root} value={root}>
3630
+ {root === "." ? "root" : root}
3631
+ </option>
3632
+ ))}
3633
+ </select>
3634
+ <input
3635
+ value={mfConsoleCommand}
3636
+ onChange={(e) => setMfConsoleCommand(e.target.value)}
3637
+ onKeyDown={(e) => {
3638
+ if (e.key === "Enter") {
3639
+ e.preventDefault();
3640
+ void runModuleFederationCommand();
3641
+ }
3642
+ }}
3643
+ disabled={!mfSandboxId || mfConsoleRunning}
3644
+ placeholder="npm run build"
3645
+ 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"
3646
+ spellCheck={false}
3647
+ />
3648
+ <button
3649
+ type="button"
3650
+ onClick={() => void runModuleFederationCommand()}
3651
+ disabled={
3652
+ !mfSandboxId ||
3653
+ mfConsoleRunning ||
3654
+ !mfConsoleCommand.trim()
3655
+ }
3656
+ 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"
3657
+ >
3658
+ {mfConsoleRunning ? (
3659
+ <Loader2 className="w-3 h-3 animate-spin" />
3660
+ ) : (
3661
+ <Play className="w-3 h-3" />
3662
+ )}
3663
+ Run
3664
+ </button>
3665
+ <button
3666
+ type="button"
3667
+ onClick={() => void refreshModuleFederationGeneratedFiles()}
3668
+ disabled={!mfSandboxId || mfConsoleRunning}
3669
+ 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"
3670
+ >
3671
+ Refresh dist
3672
+ </button>
3673
+ </div>
3674
+ <div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
3675
+ Run npm scripts in the selected webpack app. Generated dist
3676
+ files appear in the explorer as read-only artifacts.
3677
+ </div>
3678
+ <div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
3679
+ {mfConsoleOutput.length === 0 && !mfConsoleRunning && (
3680
+ <span className="text-slate-600">
3681
+ {mfSandboxId
3682
+ ? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
3683
+ : "Start webpack first, then run npm commands here."}
3684
+ </span>
3685
+ )}
3686
+ {mfConsoleOutput.map((line, index) => (
3687
+ <div key={index} className="flex items-start gap-2">
3688
+ <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
3689
+ cmd
3690
+ </span>
3691
+ <span
3692
+ className={
3693
+ line.kind === "stderr"
3694
+ ? "text-red-400 whitespace-pre-wrap"
3695
+ : line.kind === "warn"
3696
+ ? "text-amber-400 whitespace-pre-wrap"
3697
+ : line.kind === "info"
3698
+ ? "text-slate-500 italic whitespace-pre-wrap"
3699
+ : "text-slate-200 whitespace-pre-wrap"
3700
+ }
3701
+ >
3702
+ {line.text}
3703
+ </span>
3704
+ </div>
3705
+ ))}
3706
+ <div ref={mfConsoleEndRef} />
3707
+ </div>
3708
+ </div>
3709
+ )}
3710
+
3147
3711
  {/* Chat tab */}
3148
3712
  {sbxBottomTab === "chat" && (
3149
3713
  <>