create-interview-cockpit 0.7.0 → 0.8.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,31 @@ 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 getModuleFederationCommandRoots(
262
+ files: Record<string, string>,
263
+ ): string[] {
264
+ const roots = new Set<string>(["."]);
265
+
266
+ for (const filePath of Object.keys(files)) {
267
+ const match = filePath.match(/^(apps\/[^/]+)/);
268
+ if (match) {
269
+ roots.add(match[1]);
270
+ }
271
+ }
272
+
273
+ return Array.from(roots).sort((a, b) => {
274
+ if (a === ".") return -1;
275
+ if (b === ".") return 1;
276
+ return a.localeCompare(b);
277
+ });
278
+ }
279
+
280
+ type SbxBottomTab = "output" | "console" | "chat";
281
+
254
282
  export default function CodeRunnerModal() {
255
283
  const {
256
284
  closeCodeRunner,
@@ -320,14 +348,23 @@ export default function CodeRunnerModal() {
320
348
  const [mfStarting, setMfStarting] = useState(false);
321
349
  const [mfError, setMfError] = useState<string | null>(null);
322
350
  const [mfPreviewApp, setMfPreviewApp] = useState("host");
351
+ const [mfConsoleOutput, setMfConsoleOutput] = useState<OutputLine[]>([]);
352
+ const [mfConsoleCommand, setMfConsoleCommand] = useState("npm run build");
353
+ const [mfConsoleCwd, setMfConsoleCwd] = useState("apps/host");
354
+ const [mfConsoleRunning, setMfConsoleRunning] = useState(false);
355
+ const [mfGeneratedFiles, setMfGeneratedFiles] = useState<string[]>([]);
356
+ const [mfGeneratedFileContents, setMfGeneratedFileContents] = useState<
357
+ Record<string, string>
358
+ >({});
359
+ const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
323
360
  // Simulated URL bar state for Next.js mode
324
361
  const [reactPreviewPath, setReactPreviewPath] = useState("/");
325
362
  const [reactNavInput, setReactNavInput] = useState("/");
326
363
  const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
327
364
  const [reactNavIndex, setReactNavIndex] = useState(0);
328
365
 
329
- // ── Sandbox output tab ("output" | "chat") ──────────────────
330
- const [sbxBottomTab, setSbxBottomTab] = useState<"output" | "chat">("output");
366
+ // ── Sandbox output tab ("output" | "console" | "chat") ─────────────
367
+ const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
331
368
 
332
369
  // ── Sandbox panel sizes ─────────────────────────────────────────
333
370
  // sbxSplit: server pane width as % of the editor row (0–100)
@@ -499,7 +536,9 @@ export default function CodeRunnerModal() {
499
536
  };
500
537
 
501
538
  const outputEndRef = useRef<HTMLDivElement>(null);
539
+ const mfConsoleEndRef = useRef<HTMLDivElement>(null);
502
540
  const nameInputRef = useRef<HTMLInputElement>(null);
541
+ const mfGeneratedFileRequestRef = useRef<string | null>(null);
503
542
  // Tracks how many server log lines have already been flushed to sandboxOutput
504
543
  const sandboxLogOffsetRef = useRef(0);
505
544
  // Stable ref so unmount cleanup can stop sandbox without stale closure
@@ -553,6 +592,12 @@ export default function CodeRunnerModal() {
553
592
  setServerCollapsed(true);
554
593
  setClientCollapsed(false);
555
594
  setMfPreviewApp("host");
595
+ setMfConsoleCommand("npm run build");
596
+ setMfConsoleCwd("apps/host");
597
+ setMfConsoleOutput([]);
598
+ setMfGeneratedFiles([]);
599
+ setMfGeneratedFileContents({});
600
+ setMfLoadingFile(null);
556
601
  }
557
602
  }
558
603
  }, [runnerInitialSandbox]);
@@ -564,6 +609,20 @@ export default function CodeRunnerModal() {
564
609
  outputEndRef.current?.scrollIntoView({ behavior: "smooth" });
565
610
  }, [output]);
566
611
 
612
+ useEffect(() => {
613
+ if (sbxBottomTab === "console") {
614
+ mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
615
+ }
616
+ }, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
617
+
618
+ useEffect(() => {
619
+ if (clientType !== "module-federation") return;
620
+ const roots = getModuleFederationCommandRoots(reactFiles);
621
+ if (!roots.includes(mfConsoleCwd)) {
622
+ setMfConsoleCwd(roots.find((root) => root !== ".") ?? ".");
623
+ }
624
+ }, [clientType, reactFiles, mfConsoleCwd]);
625
+
567
626
  // ── Sandbox divider drag handlers ────────────────────────────────
568
627
  useEffect(() => {
569
628
  const onMove = (e: MouseEvent) => {
@@ -1038,10 +1097,51 @@ export default function CodeRunnerModal() {
1038
1097
  }
1039
1098
  }, [nxStarting, reactFiles]);
1040
1099
 
1100
+ const refreshModuleFederationGeneratedFiles = useCallback(
1101
+ async (sandboxId: string | null = mfSandboxId): Promise<string[]> => {
1102
+ if (!sandboxId) {
1103
+ setMfGeneratedFiles([]);
1104
+ setMfGeneratedFileContents({});
1105
+ setMfLoadingFile(null);
1106
+ return [];
1107
+ }
1108
+
1109
+ try {
1110
+ const files = await fetchModuleFederationGeneratedFiles(sandboxId);
1111
+ setMfGeneratedFiles(files);
1112
+ setMfGeneratedFileContents((prev) =>
1113
+ Object.fromEntries(
1114
+ Object.entries(prev).filter(([filePath]) =>
1115
+ files.includes(filePath),
1116
+ ),
1117
+ ),
1118
+ );
1119
+
1120
+ if (
1121
+ reactActiveFile &&
1122
+ !Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile) &&
1123
+ !files.includes(reactActiveFile)
1124
+ ) {
1125
+ setReactActiveFile(Object.keys(reactFiles)[0] ?? files[0] ?? "");
1126
+ }
1127
+
1128
+ return files;
1129
+ } catch (err: any) {
1130
+ setMfError(err?.message ?? String(err));
1131
+ return [];
1132
+ }
1133
+ },
1134
+ [mfSandboxId, reactActiveFile, reactFiles],
1135
+ );
1136
+
1041
1137
  const startModuleFederationServer = useCallback(async () => {
1042
1138
  if (mfStarting) return;
1043
1139
  setMfStarting(true);
1044
1140
  setMfError(null);
1141
+ setMfGeneratedFiles([]);
1142
+ setMfGeneratedFileContents({});
1143
+ setMfLoadingFile(null);
1144
+ setMfConsoleOutput([]);
1045
1145
  setSbxBottomTab("output");
1046
1146
  setSandboxOutput([
1047
1147
  {
@@ -1061,6 +1161,7 @@ export default function CodeRunnerModal() {
1061
1161
  setReactClientTab("preview");
1062
1162
  setServerCollapsed(true);
1063
1163
  setClientCollapsed(false);
1164
+ void refreshModuleFederationGeneratedFiles(info.id);
1064
1165
  setSandboxOutput((prev) => [
1065
1166
  ...prev,
1066
1167
  {
@@ -1079,7 +1180,7 @@ export default function CodeRunnerModal() {
1079
1180
  } finally {
1080
1181
  setMfStarting(false);
1081
1182
  }
1082
- }, [mfStarting, reactFiles]);
1183
+ }, [mfStarting, reactFiles, refreshModuleFederationGeneratedFiles]);
1083
1184
 
1084
1185
  const stopModuleFederationServer = useCallback(async () => {
1085
1186
  if (!mfSandboxId) return;
@@ -1088,6 +1189,10 @@ export default function CodeRunnerModal() {
1088
1189
  setMfHostUrl(null);
1089
1190
  setMfAppUrls({});
1090
1191
  setMfError(null);
1192
+ setMfGeneratedFiles([]);
1193
+ setMfGeneratedFileContents({});
1194
+ setMfLoadingFile(null);
1195
+ setMfConsoleRunning(false);
1091
1196
  setSandboxOutput((prev) => [
1092
1197
  ...prev,
1093
1198
  {
@@ -1148,6 +1253,10 @@ export default function CodeRunnerModal() {
1148
1253
  setMfHostUrl(null);
1149
1254
  setMfAppUrls({});
1150
1255
  setMfError(null);
1256
+ setMfGeneratedFiles([]);
1257
+ setMfGeneratedFileContents({});
1258
+ setMfLoadingFile(null);
1259
+ setMfConsoleRunning(false);
1151
1260
  return;
1152
1261
  }
1153
1262
  if (status.hostUrl) setMfHostUrl(status.hostUrl);
@@ -1180,6 +1289,143 @@ export default function CodeRunnerModal() {
1180
1289
  return () => clearInterval(interval);
1181
1290
  }, [mfSandboxId]);
1182
1291
 
1292
+ useEffect(() => {
1293
+ if (clientType !== "module-federation" || !mfSandboxId) return;
1294
+ if (!reactActiveFile) return;
1295
+ if (Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile)) {
1296
+ return;
1297
+ }
1298
+ if (!mfGeneratedFiles.includes(reactActiveFile)) return;
1299
+ if (mfGeneratedFileContents[reactActiveFile] !== undefined) return;
1300
+ if (mfGeneratedFileRequestRef.current === reactActiveFile) return;
1301
+
1302
+ const filePath = reactActiveFile;
1303
+ let cancelled = false;
1304
+ mfGeneratedFileRequestRef.current = filePath;
1305
+ setMfLoadingFile(filePath);
1306
+
1307
+ void fetchModuleFederationGeneratedFile(mfSandboxId, filePath)
1308
+ .then(({ content }) => {
1309
+ if (cancelled) return;
1310
+ setMfGeneratedFileContents((prev) => ({
1311
+ ...prev,
1312
+ [filePath]: content,
1313
+ }));
1314
+ })
1315
+ .catch((error: any) => {
1316
+ if (cancelled) return;
1317
+ const message = error?.message ?? String(error);
1318
+ setMfError(message);
1319
+ setMfGeneratedFileContents((prev) => ({
1320
+ ...prev,
1321
+ [filePath]: `Failed to load generated file.\n\n${message}`,
1322
+ }));
1323
+ })
1324
+ .finally(() => {
1325
+ if (mfGeneratedFileRequestRef.current === filePath) {
1326
+ mfGeneratedFileRequestRef.current = null;
1327
+ }
1328
+ if (cancelled) return;
1329
+ setMfLoadingFile((current) => (current === filePath ? null : current));
1330
+ });
1331
+
1332
+ return () => {
1333
+ cancelled = true;
1334
+ };
1335
+ }, [
1336
+ clientType,
1337
+ mfGeneratedFileContents,
1338
+ mfGeneratedFiles,
1339
+ mfSandboxId,
1340
+ reactActiveFile,
1341
+ reactFiles,
1342
+ ]);
1343
+
1344
+ const runModuleFederationCommand = useCallback(async () => {
1345
+ if (!mfSandboxId || mfConsoleRunning) return;
1346
+ const command = mfConsoleCommand.trim();
1347
+ if (!command) return;
1348
+
1349
+ setMfError(null);
1350
+ setMfConsoleRunning(true);
1351
+ setSbxBottomTab("console");
1352
+
1353
+ let streamError: string | null = null;
1354
+
1355
+ try {
1356
+ await streamModuleFederationCommand(
1357
+ {
1358
+ id: mfSandboxId,
1359
+ command,
1360
+ cwd: mfConsoleCwd === "." ? undefined : mfConsoleCwd,
1361
+ },
1362
+ (message) => {
1363
+ if (message.type === "output") {
1364
+ setMfConsoleOutput((prev) => [
1365
+ ...prev,
1366
+ {
1367
+ kind: message.kind,
1368
+ text: message.text,
1369
+ source: "server",
1370
+ },
1371
+ ]);
1372
+ return;
1373
+ }
1374
+
1375
+ if (message.type === "error") {
1376
+ streamError = message.error;
1377
+ setMfConsoleOutput((prev) => [
1378
+ ...prev,
1379
+ {
1380
+ kind: "stderr",
1381
+ text: message.error,
1382
+ source: "server",
1383
+ },
1384
+ ]);
1385
+ }
1386
+ },
1387
+ );
1388
+
1389
+ if (streamError) {
1390
+ throw new Error(streamError);
1391
+ }
1392
+
1393
+ const files = await refreshModuleFederationGeneratedFiles(mfSandboxId);
1394
+ setMfConsoleOutput((prev) => [
1395
+ ...prev,
1396
+ {
1397
+ kind: "info",
1398
+ text:
1399
+ files.length > 0
1400
+ ? `Refreshed ${files.length} generated file${files.length === 1 ? "" : "s"}.`
1401
+ : "No generated dist files were found.",
1402
+ source: "server",
1403
+ },
1404
+ ]);
1405
+ } catch (err: any) {
1406
+ const message = err?.message ?? String(err);
1407
+ setMfError(message);
1408
+ if (streamError !== message) {
1409
+ setMfConsoleOutput((prev) => [
1410
+ ...prev,
1411
+ {
1412
+ kind: "stderr",
1413
+ text: message,
1414
+ source: "server",
1415
+ },
1416
+ ]);
1417
+ }
1418
+ } finally {
1419
+ setMfConsoleRunning(false);
1420
+ }
1421
+ }, [
1422
+ mfConsoleCommand,
1423
+ mfConsoleCwd,
1424
+ mfConsoleRunning,
1425
+ mfSandboxId,
1426
+ refreshModuleFederationGeneratedFiles,
1427
+ ]);
1428
+
1183
1429
  // Clean up Next.js server when the modal is closed or mode changes away from nextjs
1184
1430
  const prevClientTypeRef = useRef(clientType);
1185
1431
  useEffect(() => {
@@ -1199,6 +1445,13 @@ export default function CodeRunnerModal() {
1199
1445
  setMfSandboxId(null);
1200
1446
  setMfHostUrl(null);
1201
1447
  setMfAppUrls({});
1448
+ setMfGeneratedFiles([]);
1449
+ setMfGeneratedFileContents({});
1450
+ setMfLoadingFile(null);
1451
+ setMfConsoleRunning(false);
1452
+ setSbxBottomTab((current) =>
1453
+ current === "console" ? "output" : current,
1454
+ );
1202
1455
  }
1203
1456
  }, [clientType, nxSandboxId, mfSandboxId]);
1204
1457
 
@@ -1215,6 +1468,16 @@ export default function CodeRunnerModal() {
1215
1468
  (ct: FrontendClientType) => {
1216
1469
  if (ct === clientType) return;
1217
1470
  setClientType(ct);
1471
+ if (ct !== "module-federation") {
1472
+ setMfGeneratedFiles([]);
1473
+ setMfGeneratedFileContents({});
1474
+ setMfLoadingFile(null);
1475
+ setMfConsoleOutput([]);
1476
+ setMfConsoleRunning(false);
1477
+ setSbxBottomTab((current) =>
1478
+ current === "console" ? "output" : current,
1479
+ );
1480
+ }
1218
1481
  if (ct !== "script") {
1219
1482
  const defs = defaultForType(ct);
1220
1483
  setReactFiles(defs.files);
@@ -1233,6 +1496,10 @@ export default function CodeRunnerModal() {
1233
1496
  setMfPreviewApp("host");
1234
1497
  setMfError(null);
1235
1498
  }
1499
+ if (ct === "module-federation") {
1500
+ setMfConsoleCommand("npm run build");
1501
+ setMfConsoleCwd("apps/host");
1502
+ }
1236
1503
  }
1237
1504
  },
1238
1505
  [clientType],
@@ -1482,6 +1749,27 @@ export default function CodeRunnerModal() {
1482
1749
  minHeight: MIN_H,
1483
1750
  };
1484
1751
 
1752
+ const moduleFederationCommandRoots =
1753
+ getModuleFederationCommandRoots(reactFiles);
1754
+ const moduleFederationGeneratedFiles = mfGeneratedFiles.filter(
1755
+ (filePath) => !Object.prototype.hasOwnProperty.call(reactFiles, filePath),
1756
+ );
1757
+ const moduleFederationGeneratedFileSet = new Set(
1758
+ moduleFederationGeneratedFiles,
1759
+ );
1760
+ const visibleReactFiles =
1761
+ clientType === "module-federation"
1762
+ ? Array.from(
1763
+ new Set([
1764
+ ...Object.keys(reactFiles),
1765
+ ...moduleFederationGeneratedFiles,
1766
+ ]),
1767
+ )
1768
+ : Object.keys(reactFiles);
1769
+ const isActiveModuleFederationGeneratedFile =
1770
+ clientType === "module-federation" &&
1771
+ moduleFederationGeneratedFileSet.has(reactActiveFile);
1772
+
1485
1773
  return (
1486
1774
  <div
1487
1775
  className="fixed z-[60] flex flex-col bg-slate-900 border border-slate-700 rounded-xl shadow-2xl overflow-hidden select-none"
@@ -2564,9 +2852,18 @@ export default function CodeRunnerModal() {
2564
2852
  {/* Tree nodes */}
2565
2853
  <div className="flex-1 py-1">
2566
2854
  {(() => {
2567
- const tree = buildFileTree(Object.keys(reactFiles));
2855
+ const tree = buildFileTree(visibleReactFiles);
2568
2856
 
2569
- const fileIcon = (name: string) => {
2857
+ const fileIcon = (
2858
+ name: string,
2859
+ isGenerated = false,
2860
+ ) => {
2861
+ if (isGenerated)
2862
+ return (
2863
+ <span className="text-emerald-400 mr-1 text-[8px] font-semibold uppercase">
2864
+ dist
2865
+ </span>
2866
+ );
2570
2867
  if (name.endsWith(".tsx") || name.endsWith(".jsx"))
2571
2868
  return (
2572
2869
  <span className="text-cyan-400 mr-1 text-[9px]">
@@ -2588,47 +2885,74 @@ export default function CodeRunnerModal() {
2588
2885
 
2589
2886
  const renderFile = (path: string, indent = 0) => (
2590
2887
  <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>
2888
+ {(() => {
2889
+ const isGenerated =
2890
+ moduleFederationGeneratedFileSet.has(path);
2891
+ return (
2892
+ <>
2893
+ <button
2894
+ type="button"
2895
+ onClick={() => {
2896
+ setReactActiveFile(path);
2897
+ setReactClientTab("edit");
2898
+ }}
2899
+ style={{
2900
+ paddingLeft: `${8 + indent * 10}px`,
2901
+ }}
2902
+ className={`flex-1 flex items-center gap-0.5 py-0.5 pr-1 text-left text-[10px] font-mono truncate transition-colors ${
2903
+ path === reactActiveFile &&
2904
+ reactClientTab === "edit"
2905
+ ? isGenerated
2906
+ ? "bg-slate-700 text-emerald-100"
2907
+ : "bg-slate-700 text-slate-100"
2908
+ : isGenerated
2909
+ ? "text-emerald-300/80 hover:bg-slate-800 hover:text-emerald-100"
2910
+ : "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
2911
+ }`}
2912
+ title={
2913
+ isGenerated
2914
+ ? `${path} (generated)`
2915
+ : path
2916
+ }
2917
+ >
2918
+ {fileIcon(
2919
+ path.split("/").pop() ?? path,
2920
+ isGenerated,
2921
+ )}
2922
+ <span className="truncate">
2923
+ {path.split("/").pop()}
2924
+ </span>
2925
+ </button>
2926
+ {!isGenerated && (
2927
+ <button
2928
+ type="button"
2929
+ onClick={() => {
2930
+ if (
2931
+ Object.keys(reactFiles).length <= 1
2932
+ )
2933
+ return;
2934
+ const remaining = Object.keys(
2935
+ reactFiles,
2936
+ ).filter((f) => f !== path);
2937
+ setReactFiles((prev) => {
2938
+ const next = { ...prev };
2939
+ delete next[path];
2940
+ return next;
2941
+ });
2942
+ if (reactActiveFile === path)
2943
+ setReactActiveFile(
2944
+ remaining[0] ?? "",
2945
+ );
2946
+ }}
2947
+ className="opacity-0 group-hover:opacity-100 p-0.5 mr-1 rounded text-slate-600 hover:text-red-400 transition-all shrink-0"
2948
+ title="Delete file"
2949
+ >
2950
+ <X className="w-2.5 h-2.5" />
2951
+ </button>
2952
+ )}
2953
+ </>
2954
+ );
2955
+ })()}
2632
2956
  </div>
2633
2957
  );
2634
2958
 
@@ -2728,25 +3052,48 @@ export default function CodeRunnerModal() {
2728
3052
  }
2729
3053
  />
2730
3054
  ) : 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
- />
3055
+ isActiveModuleFederationGeneratedFile ? (
3056
+ <div className="absolute inset-0 flex flex-col bg-slate-950">
3057
+ <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">
3058
+ Read-only build artifact from dist/. Run the webpack
3059
+ console again to refresh it.
3060
+ </div>
3061
+ <div className="flex-1 overflow-auto px-3 py-3 font-mono text-[12px] leading-relaxed text-slate-200">
3062
+ {mfLoadingFile === reactActiveFile &&
3063
+ mfGeneratedFileContents[reactActiveFile] ===
3064
+ undefined ? (
3065
+ <div className="flex h-full items-center justify-center gap-2 text-slate-500">
3066
+ <Loader2 className="w-4 h-4 animate-spin" />
3067
+ <span>Loading generated file…</span>
3068
+ </div>
3069
+ ) : (
3070
+ <pre className="m-0 min-w-max whitespace-pre">
3071
+ {mfGeneratedFileContents[reactActiveFile] ?? ""}
3072
+ </pre>
3073
+ )}
3074
+ </div>
3075
+ </div>
3076
+ ) : (
3077
+ <SyntaxEditor
3078
+ key={reactActiveFile}
3079
+ value={reactFiles[reactActiveFile] ?? ""}
3080
+ onChange={(val) =>
3081
+ setReactFiles((prev) => ({
3082
+ ...prev,
3083
+ [reactActiveFile]: val,
3084
+ }))
3085
+ }
3086
+ language={
3087
+ reactActiveFile.endsWith(".ts") ||
3088
+ reactActiveFile.endsWith(".tsx")
3089
+ ? "typescript"
3090
+ : "javascript"
3091
+ }
3092
+ fontSize="12px"
3093
+ focusRingClass="ring-cyan-500/30"
3094
+ placeholder={`// ${reactActiveFile}\n`}
3095
+ />
3096
+ )
2750
3097
  ) : (
2751
3098
  <div className="w-full h-full flex flex-col">
2752
3099
  {clientType === "nextjs" && (
@@ -3029,6 +3376,20 @@ export default function CodeRunnerModal() {
3029
3376
  ) : null}
3030
3377
  Output
3031
3378
  </button>
3379
+ {clientType === "module-federation" && (
3380
+ <button
3381
+ type="button"
3382
+ onClick={() => setSbxBottomTab("console")}
3383
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
3384
+ sbxBottomTab === "console"
3385
+ ? "border-cyan-500 text-cyan-300"
3386
+ : "border-transparent text-slate-500 hover:text-slate-300"
3387
+ }`}
3388
+ >
3389
+ <Terminal className="w-3 h-3" />
3390
+ Console
3391
+ </button>
3392
+ )}
3032
3393
  <button
3033
3394
  type="button"
3034
3395
  onClick={() => {
@@ -3049,6 +3410,9 @@ export default function CodeRunnerModal() {
3049
3410
  (serverStarting || clientRunning) && (
3050
3411
  <Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
3051
3412
  )}
3413
+ {sbxBottomTab === "console" && mfConsoleRunning && (
3414
+ <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
3415
+ )}
3052
3416
  {sbxBottomTab === "output" && sandboxOutput.length > 0 && (
3053
3417
  <div className="flex items-center gap-1 mr-1">
3054
3418
  <button
@@ -3073,6 +3437,30 @@ export default function CodeRunnerModal() {
3073
3437
  </button>
3074
3438
  </div>
3075
3439
  )}
3440
+ {sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
3441
+ <div className="flex items-center gap-1 mr-1">
3442
+ <button
3443
+ type="button"
3444
+ onClick={() =>
3445
+ navigator.clipboard.writeText(
3446
+ mfConsoleOutput.map((line) => line.text).join("\n"),
3447
+ )
3448
+ }
3449
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
3450
+ title="Copy console output"
3451
+ >
3452
+ <Copy className="w-3 h-3" />
3453
+ </button>
3454
+ <button
3455
+ type="button"
3456
+ onClick={() => setMfConsoleOutput([])}
3457
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
3458
+ title="Clear console output"
3459
+ >
3460
+ <Trash2 className="w-3 h-3" />
3461
+ </button>
3462
+ </div>
3463
+ )}
3076
3464
  {sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
3077
3465
  <button
3078
3466
  type="button"
@@ -3144,6 +3532,98 @@ export default function CodeRunnerModal() {
3144
3532
  </div>
3145
3533
  )}
3146
3534
 
3535
+ {sbxBottomTab === "console" && (
3536
+ <div className="flex-1 min-h-0 flex flex-col">
3537
+ <div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
3538
+ <select
3539
+ value={mfConsoleCwd}
3540
+ onChange={(e) => setMfConsoleCwd(e.target.value)}
3541
+ disabled={!mfSandboxId || mfConsoleRunning}
3542
+ 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"
3543
+ >
3544
+ {moduleFederationCommandRoots.map((root) => (
3545
+ <option key={root} value={root}>
3546
+ {root === "." ? "root" : root}
3547
+ </option>
3548
+ ))}
3549
+ </select>
3550
+ <input
3551
+ value={mfConsoleCommand}
3552
+ onChange={(e) => setMfConsoleCommand(e.target.value)}
3553
+ onKeyDown={(e) => {
3554
+ if (e.key === "Enter") {
3555
+ e.preventDefault();
3556
+ void runModuleFederationCommand();
3557
+ }
3558
+ }}
3559
+ disabled={!mfSandboxId || mfConsoleRunning}
3560
+ placeholder="npm run build"
3561
+ 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"
3562
+ spellCheck={false}
3563
+ />
3564
+ <button
3565
+ type="button"
3566
+ onClick={() => void runModuleFederationCommand()}
3567
+ disabled={
3568
+ !mfSandboxId ||
3569
+ mfConsoleRunning ||
3570
+ !mfConsoleCommand.trim()
3571
+ }
3572
+ 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"
3573
+ >
3574
+ {mfConsoleRunning ? (
3575
+ <Loader2 className="w-3 h-3 animate-spin" />
3576
+ ) : (
3577
+ <Play className="w-3 h-3" />
3578
+ )}
3579
+ Run
3580
+ </button>
3581
+ <button
3582
+ type="button"
3583
+ onClick={() => void refreshModuleFederationGeneratedFiles()}
3584
+ disabled={!mfSandboxId || mfConsoleRunning}
3585
+ 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"
3586
+ >
3587
+ Refresh dist
3588
+ </button>
3589
+ </div>
3590
+ <div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
3591
+ Run npm scripts in the selected webpack app. Generated dist
3592
+ files appear in the explorer as read-only artifacts.
3593
+ </div>
3594
+ <div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
3595
+ {mfConsoleOutput.length === 0 && !mfConsoleRunning && (
3596
+ <span className="text-slate-600">
3597
+ {mfSandboxId
3598
+ ? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
3599
+ : "Start webpack first, then run npm commands here."}
3600
+ </span>
3601
+ )}
3602
+ {mfConsoleOutput.map((line, index) => (
3603
+ <div key={index} className="flex items-start gap-2">
3604
+ <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
3605
+ cmd
3606
+ </span>
3607
+ <span
3608
+ className={
3609
+ line.kind === "stderr"
3610
+ ? "text-red-400 whitespace-pre-wrap"
3611
+ : line.kind === "warn"
3612
+ ? "text-amber-400 whitespace-pre-wrap"
3613
+ : line.kind === "info"
3614
+ ? "text-slate-500 italic whitespace-pre-wrap"
3615
+ : "text-slate-200 whitespace-pre-wrap"
3616
+ }
3617
+ >
3618
+ {line.text}
3619
+ </span>
3620
+ </div>
3621
+ ))}
3622
+ <div ref={mfConsoleEndRef} />
3623
+ </div>
3624
+ </div>
3625
+ )}
3626
+
3147
3627
  {/* Chat tab */}
3148
3628
  {sbxBottomTab === "chat" && (
3149
3629
  <>