create-interview-cockpit 0.9.0 → 0.11.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.
@@ -73,10 +73,67 @@ interface OutputLine {
73
73
  source?: "server" | "client";
74
74
  }
75
75
 
76
+ type MfInspectorEventKind =
77
+ | "declared-config"
78
+ | "runtime-boot"
79
+ | "route-change"
80
+ | "share-snapshot"
81
+ | "remote-load-start"
82
+ | "remote-load-success"
83
+ | "remote-load-error"
84
+ | "identity-check"
85
+ | "identity-check-error"
86
+ | "share-init-error";
87
+
88
+ interface MfInspectorEvent {
89
+ type: "mf-inspector-event";
90
+ sandboxId: string;
91
+ app: string;
92
+ runtimeId: string;
93
+ kind: MfInspectorEventKind | string;
94
+ route: string;
95
+ timestamp: number;
96
+ payload?: Record<string, unknown>;
97
+ }
98
+
76
99
  const LANG_OPTIONS = ["typescript", "javascript"] as const;
77
100
  type Lang = (typeof LANG_OPTIONS)[number];
78
101
  type FrontendClientType = "script" | FrontendLabType;
79
102
 
103
+ const MF_INSPECTOR_VIEWS = ["map", "shares", "events"] as const;
104
+
105
+ const MF_INSPECTOR_VIEW_COPY: Record<
106
+ (typeof MF_INSPECTOR_VIEWS)[number],
107
+ {
108
+ label: string;
109
+ title: string;
110
+ description: string;
111
+ tip: string;
112
+ }
113
+ > = {
114
+ map: {
115
+ label: "overview",
116
+ title: "Overview: who loaded what",
117
+ description:
118
+ "Start here. The top box is the host page. The lower boxes are remote apps. Line color and table status tell you whether the host tried to load a remote, whether the load worked, and whether React was reused safely.",
119
+ tip: "A healthy path is usually: host starts, host loads a remote, then the React copy check says same copy.",
120
+ },
121
+ shares: {
122
+ label: "shared packages",
123
+ title: "Shared packages: webpack's live reuse list",
124
+ description:
125
+ "This is the runtime truth. It shows which library versions webpack currently knows about and whether any app has started using them yet.",
126
+ tip: "For React apps, the happy path is usually one loaded React version and one loaded ReactDOM version that every app reuses.",
127
+ },
128
+ events: {
129
+ label: "timeline",
130
+ title: "Timeline: one browser event per row",
131
+ description:
132
+ "Newest items appear first. Each row tells you which app reported something, what happened in plain English, and why that step matters.",
133
+ tip: "If you're debugging a remote, follow the sequence: app started, host began loading, host finished loading, then React copy comparison.",
134
+ },
135
+ };
136
+
80
137
  // ── Sandbox default snippets ─────────────────────────────────────────
81
138
  const DEFAULT_SERVER_CODE = `import express from 'express';
82
139
  import { Readable } from 'stream';
@@ -254,6 +311,216 @@ function buildFileTree(paths: string[]): FileTreeNode {
254
311
  return root;
255
312
  }
256
313
 
314
+ function isMfInspectorEvent(value: unknown): value is MfInspectorEvent {
315
+ if (!value || typeof value !== "object") return false;
316
+ const data = value as Record<string, unknown>;
317
+ return (
318
+ data.type === "mf-inspector-event" &&
319
+ typeof data.sandboxId === "string" &&
320
+ typeof data.app === "string" &&
321
+ typeof data.runtimeId === "string" &&
322
+ typeof data.kind === "string" &&
323
+ typeof data.route === "string" &&
324
+ typeof data.timestamp === "number"
325
+ );
326
+ }
327
+
328
+ function asInspectorRecord(value: unknown): Record<string, unknown> {
329
+ return value && typeof value === "object" && !Array.isArray(value)
330
+ ? (value as Record<string, unknown>)
331
+ : {};
332
+ }
333
+
334
+ function asInspectorNumber(value: unknown): number | null {
335
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
336
+ }
337
+
338
+ function asInspectorStringArray(value: unknown): string[] {
339
+ return Array.isArray(value)
340
+ ? value.filter((entry): entry is string => typeof entry === "string")
341
+ : [];
342
+ }
343
+
344
+ function formatInspectorTimestamp(timestamp: number): string {
345
+ try {
346
+ return new Date(timestamp).toLocaleTimeString([], {
347
+ hour: "2-digit",
348
+ minute: "2-digit",
349
+ second: "2-digit",
350
+ hour12: false,
351
+ });
352
+ } catch {
353
+ return String(timestamp);
354
+ }
355
+ }
356
+
357
+ function stringifyInspectorValue(value: unknown): string {
358
+ if (typeof value === "string") return value;
359
+ if (typeof value === "number" || typeof value === "boolean") {
360
+ return String(value);
361
+ }
362
+ if (value == null) return "null";
363
+ try {
364
+ return JSON.stringify(value);
365
+ } catch {
366
+ return String(value);
367
+ }
368
+ }
369
+
370
+ function getMfInspectorBadgeClass(kind: string): string {
371
+ if (kind.includes("error")) {
372
+ return "border-red-500/30 bg-red-500/10 text-red-300";
373
+ }
374
+ if (kind === "identity-check") {
375
+ return "border-emerald-500/30 bg-emerald-500/10 text-emerald-300";
376
+ }
377
+ if (kind === "remote-load-success") {
378
+ return "border-cyan-500/30 bg-cyan-500/10 text-cyan-300";
379
+ }
380
+ if (kind === "share-snapshot" || kind === "declared-config") {
381
+ return "border-amber-500/30 bg-amber-500/10 text-amber-300";
382
+ }
383
+ return "border-slate-600 bg-slate-800/80 text-slate-300";
384
+ }
385
+
386
+ function formatMfInspectorAppLabel(app: string): string {
387
+ if (!app) return "App";
388
+ if (app === "host") return "Host app";
389
+ return `${app.charAt(0).toUpperCase()}${app.slice(1)} remote`;
390
+ }
391
+
392
+ function getMfInspectorFriendlyKindLabel(kind: string): string {
393
+ switch (kind) {
394
+ case "declared-config":
395
+ return "Setup announced";
396
+ case "runtime-boot":
397
+ return "App started in browser";
398
+ case "route-change":
399
+ return "Route changed";
400
+ case "share-snapshot":
401
+ return "Shared package snapshot";
402
+ case "remote-load-start":
403
+ return "Host started loading a remote";
404
+ case "remote-load-success":
405
+ return "Remote finished loading";
406
+ case "remote-load-error":
407
+ return "Remote load failed";
408
+ case "identity-check":
409
+ return "React copy comparison";
410
+ case "identity-check-error":
411
+ return "React comparison failed";
412
+ case "share-init-error":
413
+ return "Sharing setup failed";
414
+ default:
415
+ return kind;
416
+ }
417
+ }
418
+
419
+ function describeMfInspectorEvent(event: MfInspectorEvent): string {
420
+ const payload = asInspectorRecord(event.payload);
421
+
422
+ switch (event.kind) {
423
+ case "declared-config":
424
+ return "This app described its webpack setup: what it can expose, what other apps it can call, and which packages it wants to reuse.";
425
+ case "runtime-boot": {
426
+ const label =
427
+ typeof payload.label === "string" ? payload.label : "runtime boot";
428
+ const reactVersion =
429
+ typeof payload.reactVersion === "string"
430
+ ? payload.reactVersion
431
+ : "unknown";
432
+ return `${formatMfInspectorAppLabel(event.app)} started running in the browser. Boot label: ${label}. React version seen: ${reactVersion}.`;
433
+ }
434
+ case "route-change": {
435
+ const label =
436
+ typeof payload.label === "string" ? payload.label : "route change";
437
+ return `The page route changed, so the inspector took another snapshot of the shared packages. Trigger: ${label}.`;
438
+ }
439
+ case "share-snapshot": {
440
+ const shareScopes = asInspectorRecord(payload.shareScopes);
441
+ const packageCount = Object.entries(shareScopes).reduce(
442
+ (count, [scopeName, scopeValue]) => {
443
+ if (scopeName === "__error") return count;
444
+ return count + Object.keys(asInspectorRecord(scopeValue)).length;
445
+ },
446
+ 0,
447
+ );
448
+ return `The inspector took a fresh picture of webpack's shared-package list and saw ${packageCount} package${packageCount === 1 ? "" : "s"}.`;
449
+ }
450
+ case "remote-load-start":
451
+ return `The host started requesting ${typeof payload.remoteKey === "string" ? payload.remoteKey : "a remote module"}. This is the moment remote code begins loading into the host page.`;
452
+ case "remote-load-success": {
453
+ const remoteKey =
454
+ typeof payload.remoteKey === "string"
455
+ ? payload.remoteKey
456
+ : "remote module";
457
+ const durationMs = asInspectorNumber(payload.durationMs);
458
+ return durationMs === null
459
+ ? `The host finished loading ${remoteKey}. The remote code is now available to render.`
460
+ : `The host finished loading ${remoteKey} in ${durationMs}ms. The remote code is now available to render.`;
461
+ }
462
+ case "remote-load-error":
463
+ return `The host tried to load ${typeof payload.remoteKey === "string" ? payload.remoteKey : "a remote module"}, but it failed: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
464
+ case "identity-check": {
465
+ const remoteKey =
466
+ typeof payload.remoteKey === "string"
467
+ ? payload.remoteKey
468
+ : "remote module";
469
+ const sameReact = payload.sameReactInstance === true;
470
+ const sameDom = payload.sameReactDomInstance === true;
471
+ return `After loading ${remoteKey}, the host compared its React copies with the remote's copies. Result: React ${sameReact ? "same copy" : "different copy"}, ReactDOM ${sameDom ? "same copy" : "different copy"}.`;
472
+ }
473
+ case "identity-check-error":
474
+ return `The remote loaded, but the follow-up React comparison could not finish for ${typeof payload.remoteKey === "string" ? payload.remoteKey : "that module"}: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
475
+ case "share-init-error":
476
+ return `Webpack could not prepare its shared-package system before the check ran: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
477
+ default:
478
+ return stringifyInspectorValue(payload);
479
+ }
480
+ }
481
+
482
+ function explainMfInspectorEvent(event: MfInspectorEvent): string {
483
+ switch (event.kind) {
484
+ case "declared-config":
485
+ return "This is the app's promise before runtime. It tells you what should be possible even before the browser loads anything.";
486
+ case "runtime-boot":
487
+ return "If you do not see this, that app never really started in the browser.";
488
+ case "route-change":
489
+ return "Some remotes load only after navigation, so route changes can explain why behavior changed later.";
490
+ case "share-snapshot":
491
+ return "This helps you compare what webpack is actually sharing right now versus what the config files claimed.";
492
+ case "remote-load-start":
493
+ return "If a later success row never appears, the host got stuck while trying to reach the remote.";
494
+ case "remote-load-success":
495
+ return "This proves the remote code arrived. It does not yet prove React is being shared safely.";
496
+ case "remote-load-error":
497
+ return "Common causes are a bad remote URL, a wrong expose name, or a remote build that is not serving correctly.";
498
+ case "identity-check":
499
+ return "The safest result is same copy for both React and ReactDOM. Different copies often lead to hooks, context, or state bugs.";
500
+ case "identity-check-error":
501
+ return "The remote may have loaded, but the inspector could not prove whether React was reused correctly.";
502
+ case "share-init-error":
503
+ return "Without this setup step, later sharing information may be incomplete or misleading.";
504
+ default:
505
+ return "This row is extra debug detail reported by the running apps.";
506
+ }
507
+ }
508
+
509
+ function areMfInspectorEventsEquivalent(
510
+ left: MfInspectorEvent,
511
+ right: MfInspectorEvent,
512
+ ): boolean {
513
+ return (
514
+ left.runtimeId === right.runtimeId &&
515
+ left.app === right.app &&
516
+ left.kind === right.kind &&
517
+ left.timestamp === right.timestamp &&
518
+ left.route === right.route &&
519
+ stringifyInspectorValue(left.payload) ===
520
+ stringifyInspectorValue(right.payload)
521
+ );
522
+ }
523
+
257
524
  function isModuleFederationGeneratedPath(filePath: string): boolean {
258
525
  return /(^|\/)dist\//.test(filePath);
259
526
  }
@@ -283,7 +550,7 @@ function getModuleFederationCommandRoots(
283
550
  });
284
551
  }
285
552
 
286
- type SbxBottomTab = "output" | "console" | "chat";
553
+ type SbxBottomTab = "output" | "console" | "inspector" | "chat";
287
554
 
288
555
  export default function CodeRunnerModal() {
289
556
  const {
@@ -366,6 +633,14 @@ export default function CodeRunnerModal() {
366
633
  Record<string, string>
367
634
  >({});
368
635
  const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
636
+ const [mfInspectorEvents, setMfInspectorEvents] = useState<
637
+ MfInspectorEvent[]
638
+ >([]);
639
+ const [mfInspectorView, setMfInspectorView] = useState<
640
+ "map" | "shares" | "events"
641
+ >("map");
642
+ const [mfInspectorGuideCollapsed, setMfInspectorGuideCollapsed] =
643
+ useState(false);
369
644
  // Simulated URL bar state for Next.js mode
370
645
  const [reactPreviewPath, setReactPreviewPath] = useState("/");
371
646
  const [reactNavInput, setReactNavInput] = useState("/");
@@ -389,6 +664,7 @@ export default function CodeRunnerModal() {
389
664
  containerW: number;
390
665
  } | null>(null);
391
666
  const sbxOutputDrag = useRef<{ startY: number; startH: number } | null>(null);
667
+ const [isDraggingResize, setIsDraggingResize] = useState(false);
392
668
  const sbxEditorRowRef = useRef<HTMLDivElement>(null);
393
669
 
394
670
  // ── Sandbox save state (single combined save) ──────────────────
@@ -609,6 +885,7 @@ export default function CodeRunnerModal() {
609
885
  setMfGeneratedFiles([]);
610
886
  setMfGeneratedFileContents({});
611
887
  setMfLoadingFile(null);
888
+ setMfInspectorEvents([]);
612
889
  }
613
890
  }
614
891
  }, [runnerInitialSandbox]);
@@ -653,14 +930,22 @@ export default function CodeRunnerModal() {
653
930
  }
654
931
  };
655
932
  const onUp = () => {
656
- sbxDividerDrag.current = null;
657
- sbxOutputDrag.current = null;
933
+ if (sbxDividerDrag.current || sbxOutputDrag.current) {
934
+ sbxDividerDrag.current = null;
935
+ sbxOutputDrag.current = null;
936
+ document.body.style.userSelect = "";
937
+ document.body.style.cursor = "";
938
+ setIsDraggingResize(false);
939
+ }
658
940
  };
659
941
  document.addEventListener("mousemove", onMove);
660
942
  document.addEventListener("mouseup", onUp);
661
943
  return () => {
662
944
  document.removeEventListener("mousemove", onMove);
663
945
  document.removeEventListener("mouseup", onUp);
946
+ document.body.style.userSelect = "";
947
+ document.body.style.cursor = "";
948
+ setIsDraggingResize(false);
664
949
  };
665
950
  }, []);
666
951
 
@@ -1153,6 +1438,7 @@ export default function CodeRunnerModal() {
1153
1438
  setMfGeneratedFileContents({});
1154
1439
  setMfLoadingFile(null);
1155
1440
  setMfConsoleOutput([]);
1441
+ setMfInspectorEvents([]);
1156
1442
  setSbxBottomTab("output");
1157
1443
  setSandboxOutput([
1158
1444
  {
@@ -1206,6 +1492,7 @@ export default function CodeRunnerModal() {
1206
1492
  setMfGeneratedFileContents({});
1207
1493
  setMfLoadingFile(null);
1208
1494
  setMfConsoleRunning(false);
1495
+ setMfInspectorEvents([]);
1209
1496
  setSandboxOutput((prev) => [
1210
1497
  ...prev,
1211
1498
  {
@@ -1302,6 +1589,34 @@ export default function CodeRunnerModal() {
1302
1589
  return () => clearInterval(interval);
1303
1590
  }, [mfSandboxId]);
1304
1591
 
1592
+ useEffect(() => {
1593
+ if (clientType !== "module-federation" || !mfSandboxId) return;
1594
+
1595
+ const handler = (event: MessageEvent) => {
1596
+ if (!isMfInspectorEvent(event.data)) return;
1597
+ if (event.data.sandboxId !== mfSandboxId) return;
1598
+
1599
+ const payload = asInspectorRecord(event.data.payload);
1600
+ const nextEvent: MfInspectorEvent = {
1601
+ ...event.data,
1602
+ payload,
1603
+ };
1604
+
1605
+ setMfInspectorEvents((prev) => {
1606
+ const duplicate = prev.some((entry) =>
1607
+ areMfInspectorEventsEquivalent(entry, nextEvent),
1608
+ );
1609
+ if (duplicate) return prev;
1610
+
1611
+ const next = [...prev, nextEvent];
1612
+ return next.length > 250 ? next.slice(next.length - 250) : next;
1613
+ });
1614
+ };
1615
+
1616
+ window.addEventListener("message", handler);
1617
+ return () => window.removeEventListener("message", handler);
1618
+ }, [clientType, mfSandboxId]);
1619
+
1305
1620
  useEffect(() => {
1306
1621
  if (clientType !== "module-federation" || !mfSandboxId) return;
1307
1622
  if (!reactActiveFile) return;
@@ -1462,8 +1777,9 @@ export default function CodeRunnerModal() {
1462
1777
  setMfGeneratedFileContents({});
1463
1778
  setMfLoadingFile(null);
1464
1779
  setMfConsoleRunning(false);
1780
+ setMfInspectorEvents([]);
1465
1781
  setSbxBottomTab((current) =>
1466
- current === "console" ? "output" : current,
1782
+ current === "console" || current === "inspector" ? "output" : current,
1467
1783
  );
1468
1784
  }
1469
1785
  }, [clientType, nxSandboxId, mfSandboxId]);
@@ -1487,8 +1803,9 @@ export default function CodeRunnerModal() {
1487
1803
  setMfLoadingFile(null);
1488
1804
  setMfConsoleOutput([]);
1489
1805
  setMfConsoleRunning(false);
1806
+ setMfInspectorEvents([]);
1490
1807
  setSbxBottomTab((current) =>
1491
- current === "console" ? "output" : current,
1808
+ current === "console" || current === "inspector" ? "output" : current,
1492
1809
  );
1493
1810
  }
1494
1811
  if (ct !== "script") {
@@ -1794,6 +2111,590 @@ export default function CodeRunnerModal() {
1794
2111
  const isActiveModuleFederationGeneratedFile =
1795
2112
  clientType === "module-federation" &&
1796
2113
  moduleFederationGeneratedFileSet.has(reactActiveFile);
2114
+ const mfInspectorRuntimeCount = new Set(
2115
+ mfInspectorEvents.map((event) => event.runtimeId),
2116
+ ).size;
2117
+ const mfInspectorLastEvent =
2118
+ mfInspectorEvents[mfInspectorEvents.length - 1] ?? null;
2119
+ const mfInspectorDeclaredConfigMap = new Map<
2120
+ string,
2121
+ {
2122
+ app: string;
2123
+ declaredConfig: Record<string, unknown>;
2124
+ timestamp: number;
2125
+ source: string;
2126
+ }
2127
+ >();
2128
+ const mfInspectorShareScopeMap = new Map<
2129
+ string,
2130
+ {
2131
+ app: string;
2132
+ shareScopes: Record<string, unknown>;
2133
+ timestamp: number;
2134
+ route: string;
2135
+ source: string;
2136
+ runtimeId: string;
2137
+ }
2138
+ >();
2139
+ const mfInspectorIdentityEvents: MfInspectorEvent[] = [];
2140
+
2141
+ for (const event of mfInspectorEvents) {
2142
+ const payload = asInspectorRecord(event.payload);
2143
+ const declaredConfig = asInspectorRecord(payload.declaredConfig);
2144
+ if (Object.keys(declaredConfig).length > 0) {
2145
+ const configApp =
2146
+ event.kind === "identity-check" && typeof payload.remoteApp === "string"
2147
+ ? payload.remoteApp
2148
+ : event.app;
2149
+ mfInspectorDeclaredConfigMap.set(configApp, {
2150
+ app: configApp,
2151
+ declaredConfig,
2152
+ timestamp: event.timestamp,
2153
+ source: event.kind,
2154
+ });
2155
+ }
2156
+
2157
+ const shareScopes = asInspectorRecord(payload.shareScopes);
2158
+ if (Object.keys(shareScopes).length > 0) {
2159
+ mfInspectorShareScopeMap.set(event.app, {
2160
+ app: event.app,
2161
+ shareScopes,
2162
+ timestamp: event.timestamp,
2163
+ route: event.route,
2164
+ source: event.kind,
2165
+ runtimeId: event.runtimeId,
2166
+ });
2167
+ }
2168
+
2169
+ if (
2170
+ event.kind === "identity-check" ||
2171
+ event.kind === "identity-check-error"
2172
+ ) {
2173
+ mfInspectorIdentityEvents.push(event);
2174
+ }
2175
+ }
2176
+
2177
+ const mfInspectorDeclaredConfigs = Array.from(
2178
+ mfInspectorDeclaredConfigMap.values(),
2179
+ ).sort((left, right) => left.app.localeCompare(right.app));
2180
+ const mfInspectorShareSnapshots = Array.from(
2181
+ mfInspectorShareScopeMap.values(),
2182
+ ).sort((left, right) => left.app.localeCompare(right.app));
2183
+ const mfInspectorRecentEvents = [...mfInspectorEvents].slice(-60).reverse();
2184
+
2185
+ // ── Inspector topology derived data ──────────────────────────────────────
2186
+ const mfInspectorBootMap = new Map<
2187
+ string,
2188
+ {
2189
+ reactVersion: string | null;
2190
+ reactDomVersion: string | null;
2191
+ timestamp: number;
2192
+ }
2193
+ >();
2194
+ const mfInspectorRemoteLoadMap = new Map<
2195
+ string,
2196
+ {
2197
+ status: "loading" | "success" | "error";
2198
+ durationMs: number | null;
2199
+ message: string | null;
2200
+ }
2201
+ >();
2202
+ const mfInspectorIdentityResultMap = new Map<
2203
+ string,
2204
+ {
2205
+ sameReact: boolean;
2206
+ sameReactDom: boolean;
2207
+ remoteApp: string;
2208
+ reactVersion: string | null;
2209
+ reactDomVersion: string | null;
2210
+ error: string | null;
2211
+ }
2212
+ >();
2213
+
2214
+ for (const ev of mfInspectorEvents) {
2215
+ const ep = asInspectorRecord(ev.payload);
2216
+ if (ev.kind === "runtime-boot") {
2217
+ mfInspectorBootMap.set(ev.app, {
2218
+ reactVersion:
2219
+ typeof ep.reactVersion === "string" ? ep.reactVersion : null,
2220
+ reactDomVersion:
2221
+ typeof ep.reactDomVersion === "string" ? ep.reactDomVersion : null,
2222
+ timestamp: ev.timestamp,
2223
+ });
2224
+ }
2225
+ if (ev.kind === "remote-load-start") {
2226
+ const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
2227
+ // Always reset to "loading" — a fresh load-start means a retry is in
2228
+ // progress (e.g. after the user reloads the preview), so any previous
2229
+ // error/success state should be replaced.
2230
+ mfInspectorRemoteLoadMap.set(rk, {
2231
+ status: "loading",
2232
+ durationMs: null,
2233
+ message: null,
2234
+ });
2235
+ }
2236
+ if (ev.kind === "remote-load-success") {
2237
+ const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
2238
+ mfInspectorRemoteLoadMap.set(rk, {
2239
+ status: "success",
2240
+ durationMs: asInspectorNumber(ep.durationMs),
2241
+ message: null,
2242
+ });
2243
+ }
2244
+ if (ev.kind === "remote-load-error") {
2245
+ const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
2246
+ mfInspectorRemoteLoadMap.set(rk, {
2247
+ status: "error",
2248
+ durationMs: null,
2249
+ message: typeof ep.message === "string" ? ep.message : "error",
2250
+ });
2251
+ }
2252
+ }
2253
+
2254
+ for (const ev of mfInspectorIdentityEvents) {
2255
+ const ep = asInspectorRecord(ev.payload);
2256
+ const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
2257
+ if (ev.kind === "identity-check") {
2258
+ mfInspectorIdentityResultMap.set(rk, {
2259
+ sameReact: ep.sameReactInstance === true,
2260
+ sameReactDom: ep.sameReactDomInstance === true,
2261
+ remoteApp:
2262
+ typeof ep.remoteApp === "string"
2263
+ ? ep.remoteApp
2264
+ : rk.split("/")[0] || "unknown",
2265
+ reactVersion:
2266
+ typeof ep.reactVersion === "string" ? ep.reactVersion : null,
2267
+ reactDomVersion:
2268
+ typeof ep.reactDomVersion === "string" ? ep.reactDomVersion : null,
2269
+ error: null,
2270
+ });
2271
+ }
2272
+ if (ev.kind === "identity-check-error") {
2273
+ mfInspectorIdentityResultMap.set(rk, {
2274
+ sameReact: false,
2275
+ sameReactDom: false,
2276
+ remoteApp: rk.split("/")[0] || "unknown",
2277
+ reactVersion: null,
2278
+ reactDomVersion: null,
2279
+ error: typeof ep.message === "string" ? ep.message : "error",
2280
+ });
2281
+ }
2282
+ }
2283
+
2284
+ // Remote app names for the topology
2285
+ const mfTopologyRemoteNames = Array.from(
2286
+ new Set([
2287
+ ...mfInspectorDeclaredConfigs
2288
+ .filter((c) => c.app !== "host")
2289
+ .map((c) => c.app),
2290
+ ...Array.from(mfInspectorBootMap.keys()).filter((k) => k !== "host"),
2291
+ ...Array.from(mfInspectorShareScopeMap.keys()).filter(
2292
+ (k) => k !== "host",
2293
+ ),
2294
+ ]),
2295
+ );
2296
+
2297
+ // Remote keys grouped by remote app name (e.g. "profile" → ["profile/ProfileCard"])
2298
+ // Sources: load events (from host loading), identity events (from host identity check),
2299
+ // and declared config (so declared-but-unloaded remotes are always wired up).
2300
+ const mfTopologyRemoteKeysByApp = new Map<string, string[]>();
2301
+ const _addRemoteKey = (remoteKey: string) => {
2302
+ const appName = remoteKey.split("/")[0] || remoteKey;
2303
+ if (!mfTopologyRemoteKeysByApp.has(appName)) {
2304
+ mfTopologyRemoteKeysByApp.set(appName, []);
2305
+ }
2306
+ const existing = mfTopologyRemoteKeysByApp.get(appName)!;
2307
+ if (!existing.includes(remoteKey)) existing.push(remoteKey);
2308
+ };
2309
+ // From load events
2310
+ for (const [remoteKey] of mfInspectorRemoteLoadMap) {
2311
+ _addRemoteKey(remoteKey);
2312
+ }
2313
+ // From identity events (covers the case where load events were missed)
2314
+ for (const [remoteKey] of mfInspectorIdentityResultMap) {
2315
+ _addRemoteKey(remoteKey);
2316
+ }
2317
+ // From declared config — so every declared remote gets a key even before any events
2318
+ for (const { app: cfgApp, declaredConfig } of mfInspectorDeclaredConfigs) {
2319
+ if (cfgApp !== "host") continue; // only the host declares remotes it consumes
2320
+ const remotes = asInspectorRecord(declaredConfig.remotes);
2321
+ for (const remoteName of Object.keys(remotes)) {
2322
+ // Derive the canonical remote key from declared exposes, or fall back to
2323
+ // the key pattern seen in load/identity events for this remote app.
2324
+ const existingKeys = mfTopologyRemoteKeysByApp.get(remoteName);
2325
+ if (!existingKeys || existingKeys.length === 0) {
2326
+ // Find any declared config for the remote itself to get its expose list
2327
+ const remoteCfg = mfInspectorDeclaredConfigs.find(
2328
+ (c) => c.app === remoteName,
2329
+ );
2330
+ const exposes = asInspectorRecord(
2331
+ remoteCfg ? remoteCfg.declaredConfig.exposes : {},
2332
+ );
2333
+ const exposeKeys = Object.keys(exposes);
2334
+ if (exposeKeys.length > 0) {
2335
+ for (const exposeKey of exposeKeys) {
2336
+ // exposeKey is like "./ProfileCard"; canonical remote key is "profile/ProfileCard"
2337
+ const moduleName = exposeKey.replace(/^\.\//, "");
2338
+ _addRemoteKey(`${remoteName}/${moduleName}`);
2339
+ }
2340
+ } else {
2341
+ // No expose info yet — create a placeholder key so the node appears
2342
+ _addRemoteKey(`${remoteName}/__placeholder`);
2343
+ }
2344
+ }
2345
+ }
2346
+ }
2347
+
2348
+ // Best share scope snapshot for the matrix (host has the most complete view)
2349
+ const mfShareScopeForMatrix =
2350
+ mfInspectorShareScopeMap.get("host") ??
2351
+ (mfInspectorShareSnapshots.length > 0
2352
+ ? mfInspectorShareSnapshots[mfInspectorShareSnapshots.length - 1]
2353
+ : null);
2354
+ const mfInspectorRemoteTableKeys = Array.from(
2355
+ new Set([
2356
+ ...Array.from(mfInspectorRemoteLoadMap.keys()),
2357
+ ...Array.from(mfInspectorIdentityResultMap.keys()),
2358
+ ]),
2359
+ )
2360
+ .filter((remoteKey) => !remoteKey.endsWith("/__placeholder"))
2361
+ .sort((left, right) => left.localeCompare(right));
2362
+ const mfInspectorLoadAttemptCount = mfInspectorRemoteTableKeys.filter(
2363
+ (remoteKey) => mfInspectorRemoteLoadMap.has(remoteKey),
2364
+ ).length;
2365
+ const mfInspectorSharedRemoteCount = mfInspectorRemoteTableKeys.filter(
2366
+ (remoteKey) => {
2367
+ const result = mfInspectorIdentityResultMap.get(remoteKey);
2368
+ return Boolean(
2369
+ result && !result.error && result.sameReact && result.sameReactDom,
2370
+ );
2371
+ },
2372
+ ).length;
2373
+ const mfInspectorDifferentRemoteCount = mfInspectorRemoteTableKeys.filter(
2374
+ (remoteKey) => {
2375
+ const result = mfInspectorIdentityResultMap.get(remoteKey);
2376
+ return Boolean(
2377
+ result && !result.error && (!result.sameReact || !result.sameReactDom),
2378
+ );
2379
+ },
2380
+ ).length;
2381
+ const mfInspectorErroredRemoteCount = mfInspectorRemoteTableKeys.filter(
2382
+ (remoteKey) => {
2383
+ const result = mfInspectorIdentityResultMap.get(remoteKey);
2384
+ const loadInfo = mfInspectorRemoteLoadMap.get(remoteKey);
2385
+ return Boolean(result?.error) || loadInfo?.status === "error";
2386
+ },
2387
+ ).length;
2388
+ const mfInspectorPendingRemoteCount = mfInspectorRemoteTableKeys.filter(
2389
+ (remoteKey) => {
2390
+ const result = mfInspectorIdentityResultMap.get(remoteKey);
2391
+ const loadInfo = mfInspectorRemoteLoadMap.get(remoteKey);
2392
+ return (
2393
+ !result &&
2394
+ (loadInfo?.status === "loading" || loadInfo?.status === "success")
2395
+ );
2396
+ },
2397
+ ).length;
2398
+ const mfInspectorIssueCount =
2399
+ mfInspectorDifferentRemoteCount + mfInspectorErroredRemoteCount;
2400
+ const mfInspectorViewCopy = MF_INSPECTOR_VIEW_COPY[mfInspectorView];
2401
+ const mfInspectorWorkspacePackageMap = new Map<
2402
+ string,
2403
+ {
2404
+ apps: string[];
2405
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2406
+ }
2407
+ >();
2408
+
2409
+ for (const [filePath, fileContents] of Object.entries(reactFiles)) {
2410
+ const match = filePath.match(/^apps\/([^/]+)\/package\.json$/);
2411
+ if (!match) continue;
2412
+
2413
+ try {
2414
+ const packageJson = JSON.parse(fileContents);
2415
+ const appName = match[1];
2416
+ const dependencyVersions = asInspectorRecord(packageJson.dependencies);
2417
+
2418
+ for (const [packageName, versionValue] of Object.entries(
2419
+ dependencyVersions,
2420
+ )) {
2421
+ const existing = mfInspectorWorkspacePackageMap.get(packageName) ?? {
2422
+ apps: [],
2423
+ requiredVersions: [],
2424
+ };
2425
+
2426
+ if (!existing.apps.includes(appName)) {
2427
+ existing.apps.push(appName);
2428
+ }
2429
+ existing.requiredVersions.push({
2430
+ app: appName,
2431
+ version: typeof versionValue === "string" ? versionValue : null,
2432
+ });
2433
+ mfInspectorWorkspacePackageMap.set(packageName, existing);
2434
+ }
2435
+ } catch {
2436
+ // Ignore malformed package.json edits while the user is typing.
2437
+ }
2438
+ }
2439
+
2440
+ const mfInspectorDeclaredSharedPackageMap = new Map<
2441
+ string,
2442
+ {
2443
+ apps: string[];
2444
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2445
+ singletonPreferred: boolean;
2446
+ }
2447
+ >();
2448
+
2449
+ for (const [packageName, packageInfo] of mfInspectorWorkspacePackageMap) {
2450
+ mfInspectorDeclaredSharedPackageMap.set(packageName, {
2451
+ apps: [...packageInfo.apps],
2452
+ requiredVersions: [...packageInfo.requiredVersions],
2453
+ singletonPreferred: false,
2454
+ });
2455
+ }
2456
+
2457
+ for (const { app, declaredConfig } of mfInspectorDeclaredConfigs) {
2458
+ const sharedPackages = asInspectorRecord(declaredConfig.shared);
2459
+ for (const packageName of Object.keys(sharedPackages)) {
2460
+ const packageConfig = asInspectorRecord(sharedPackages[packageName]);
2461
+ const existing = mfInspectorDeclaredSharedPackageMap.get(packageName) ?? {
2462
+ apps: [],
2463
+ requiredVersions: [],
2464
+ singletonPreferred: false,
2465
+ };
2466
+
2467
+ if (!existing.apps.includes(app)) {
2468
+ existing.apps.push(app);
2469
+ }
2470
+ existing.requiredVersions.push({
2471
+ app,
2472
+ version:
2473
+ typeof packageConfig.requiredVersion === "string"
2474
+ ? packageConfig.requiredVersion
2475
+ : packageConfig.requiredVersion === false
2476
+ ? false
2477
+ : null,
2478
+ });
2479
+ existing.singletonPreferred =
2480
+ existing.singletonPreferred || packageConfig.singleton === true;
2481
+ mfInspectorDeclaredSharedPackageMap.set(packageName, existing);
2482
+ }
2483
+ }
2484
+
2485
+ const mfInspectorShareScopeNames = Array.from(
2486
+ new Set([
2487
+ ...Object.keys(mfShareScopeForMatrix?.shareScopes ?? {}).filter(
2488
+ (scopeName) => scopeName !== "__error",
2489
+ ),
2490
+ ...(mfInspectorDeclaredSharedPackageMap.size > 0 ? ["default"] : []),
2491
+ ]),
2492
+ );
2493
+ const mfInspectorRuntimeSharedPackageMap = new Map<
2494
+ string,
2495
+ { registeredVersionCount: number; loaded: boolean }
2496
+ >();
2497
+
2498
+ for (const [scopeName, scopeValue] of Object.entries(
2499
+ mfShareScopeForMatrix?.shareScopes ?? {},
2500
+ )) {
2501
+ if (scopeName === "__error") continue;
2502
+ for (const [packageName, versionValue] of Object.entries(
2503
+ asInspectorRecord(scopeValue),
2504
+ )) {
2505
+ const versionEntries = Array.isArray(versionValue) ? versionValue : [];
2506
+ const existing = mfInspectorRuntimeSharedPackageMap.get(packageName) ?? {
2507
+ registeredVersionCount: 0,
2508
+ loaded: false,
2509
+ };
2510
+ existing.registeredVersionCount = Math.max(
2511
+ existing.registeredVersionCount,
2512
+ versionEntries.length,
2513
+ );
2514
+ existing.loaded =
2515
+ existing.loaded ||
2516
+ versionEntries.some(
2517
+ (entry) => asInspectorRecord(entry).loaded === true,
2518
+ );
2519
+ mfInspectorRuntimeSharedPackageMap.set(packageName, existing);
2520
+ }
2521
+ }
2522
+
2523
+ const mfInspectorSharedPackagesByApp = new Map<
2524
+ string,
2525
+ Array<{
2526
+ packageName: string;
2527
+ singletonPreferred: boolean;
2528
+ runtimeRegistered: boolean;
2529
+ runtimeLoaded: boolean;
2530
+ }>
2531
+ >();
2532
+
2533
+ for (const [
2534
+ packageName,
2535
+ packageInfo,
2536
+ ] of mfInspectorDeclaredSharedPackageMap) {
2537
+ for (const appName of packageInfo.apps) {
2538
+ const appPackages = mfInspectorSharedPackagesByApp.get(appName) ?? [];
2539
+ const runtimePackageInfo =
2540
+ mfInspectorRuntimeSharedPackageMap.get(packageName) ?? null;
2541
+ appPackages.push({
2542
+ packageName,
2543
+ singletonPreferred: packageInfo.singletonPreferred,
2544
+ runtimeRegistered: runtimePackageInfo != null,
2545
+ runtimeLoaded: runtimePackageInfo?.loaded === true,
2546
+ });
2547
+ mfInspectorSharedPackagesByApp.set(appName, appPackages);
2548
+ }
2549
+ }
2550
+
2551
+ for (const packages of mfInspectorSharedPackagesByApp.values()) {
2552
+ packages.sort((left, right) =>
2553
+ left.packageName.localeCompare(right.packageName),
2554
+ );
2555
+ }
2556
+ const mfInspectorSummaryCards = [
2557
+ {
2558
+ label: "Host page",
2559
+ value: mfInspectorBootMap.has("host") ? "running" : "waiting",
2560
+ toneClass: mfInspectorBootMap.has("host")
2561
+ ? "text-sky-300"
2562
+ : "text-amber-300",
2563
+ hint: "The host is the page that loads remotes.",
2564
+ },
2565
+ {
2566
+ label: "Remote modules",
2567
+ value: String(mfInspectorRemoteTableKeys.length),
2568
+ toneClass: "text-slate-100",
2569
+ hint: "Separate pieces of remote code the host has seen.",
2570
+ },
2571
+ {
2572
+ label: "Load attempts",
2573
+ value: String(mfInspectorLoadAttemptCount),
2574
+ toneClass:
2575
+ mfInspectorLoadAttemptCount > 0 ? "text-cyan-300" : "text-slate-400",
2576
+ hint: "How many remote modules the host actually tried to fetch.",
2577
+ },
2578
+ {
2579
+ label: "Same React copy",
2580
+ value: String(mfInspectorSharedRemoteCount),
2581
+ toneClass:
2582
+ mfInspectorSharedRemoteCount > 0
2583
+ ? "text-emerald-300"
2584
+ : "text-slate-400",
2585
+ hint: "Healthy result after the comparison step.",
2586
+ },
2587
+ {
2588
+ label: "Waiting / issues",
2589
+ value: `${mfInspectorPendingRemoteCount} / ${mfInspectorIssueCount}`,
2590
+ toneClass:
2591
+ mfInspectorIssueCount > 0
2592
+ ? "text-red-300"
2593
+ : mfInspectorPendingRemoteCount > 0
2594
+ ? "text-amber-300"
2595
+ : "text-emerald-300",
2596
+ hint: "First number is still loading/checking. Second number needs attention.",
2597
+ },
2598
+ ];
2599
+
2600
+ const getMfInspectorAppSharedPackages = (appName: string) =>
2601
+ mfInspectorSharedPackagesByApp.get(appName) ?? [];
2602
+
2603
+ const getMfInspectorAppSharedPackageSummary = (appName: string) => {
2604
+ const packages = getMfInspectorAppSharedPackages(appName);
2605
+ const previewItems = packages.slice(0, 2).map((entry) => entry.packageName);
2606
+ const hiddenCount = packages.length - previewItems.length;
2607
+
2608
+ return {
2609
+ count: packages.length,
2610
+ countLabel:
2611
+ packages.length === 1
2612
+ ? "1 shared package"
2613
+ : `${packages.length} shared packages`,
2614
+ previewText:
2615
+ packages.length === 0
2616
+ ? "none declared"
2617
+ : previewItems.join(", ") +
2618
+ (hiddenCount > 0 ? ` +${hiddenCount}` : ""),
2619
+ };
2620
+ };
2621
+
2622
+ const renderMfInspectorPackageBadges = (appName: string) => {
2623
+ const packages = getMfInspectorAppSharedPackages(appName);
2624
+
2625
+ if (packages.length === 0) {
2626
+ return <span className="text-slate-600">none declared</span>;
2627
+ }
2628
+
2629
+ return (
2630
+ <div className="flex flex-wrap gap-1">
2631
+ {packages.map((entry) => {
2632
+ const toneClass = entry.runtimeLoaded
2633
+ ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-200"
2634
+ : entry.runtimeRegistered
2635
+ ? "border-cyan-500/30 bg-cyan-500/10 text-cyan-200"
2636
+ : "border-slate-700 bg-slate-800/50 text-slate-300";
2637
+ return (
2638
+ <span
2639
+ key={`${appName}-${entry.packageName}`}
2640
+ className={`rounded border px-1.5 py-0.5 text-[10px] font-mono ${toneClass}`}
2641
+ title={
2642
+ entry.runtimeLoaded
2643
+ ? "Seen and loaded at runtime"
2644
+ : entry.runtimeRegistered
2645
+ ? "Seen in webpack runtime registry"
2646
+ : "Declared in package.json but not yet seen at runtime"
2647
+ }
2648
+ >
2649
+ {entry.packageName}
2650
+ {entry.singletonPreferred ? " · one copy" : ""}
2651
+ </span>
2652
+ );
2653
+ })}
2654
+ </div>
2655
+ );
2656
+ };
2657
+
2658
+ const renderMfInspectorTruthCell = (
2659
+ sameInstance: boolean | null,
2660
+ error: string | null,
2661
+ loadState: "loading" | "success" | "error" | null,
2662
+ ) => {
2663
+ let label = "not requested";
2664
+ let toneClass = "text-slate-400";
2665
+ let dotClass = "bg-slate-500";
2666
+
2667
+ if (error || loadState === "error") {
2668
+ label = "load failed";
2669
+ toneClass = "text-red-400";
2670
+ dotClass = "bg-red-400";
2671
+ } else if (sameInstance === true) {
2672
+ label = "same copy";
2673
+ toneClass = "text-emerald-400";
2674
+ dotClass = "bg-emerald-400";
2675
+ } else if (sameInstance === false) {
2676
+ label = "different copy";
2677
+ toneClass = "text-red-400";
2678
+ dotClass = "bg-red-400";
2679
+ } else if (loadState === "loading") {
2680
+ label = "loading";
2681
+ toneClass = "text-amber-400";
2682
+ dotClass = "bg-amber-400";
2683
+ } else if (loadState === "success") {
2684
+ label = "checking React";
2685
+ toneClass = "text-amber-400";
2686
+ dotClass = "bg-amber-400";
2687
+ }
2688
+
2689
+ return (
2690
+ <span
2691
+ className={`inline-flex items-center gap-1 text-[10px] font-medium ${toneClass}`}
2692
+ >
2693
+ <span className={`w-1.5 h-1.5 rounded-full ${dotClass}`} />
2694
+ {label}
2695
+ </span>
2696
+ );
2697
+ };
1797
2698
 
1798
2699
  return (
1799
2700
  <div
@@ -2423,6 +3324,9 @@ export default function CodeRunnerModal() {
2423
3324
  startPct: sbxSplit,
2424
3325
  containerW,
2425
3326
  };
3327
+ document.body.style.userSelect = "none";
3328
+ document.body.style.cursor = "col-resize";
3329
+ setIsDraggingResize(true);
2426
3330
  }}
2427
3331
  />
2428
3332
  )}
@@ -3260,7 +4164,26 @@ export default function CodeRunnerModal() {
3260
4164
  <button
3261
4165
  key={name}
3262
4166
  type="button"
3263
- onClick={() => setMfPreviewApp(name)}
4167
+ onClick={() => {
4168
+ setMfPreviewApp(name);
4169
+ // Always imperatively set the iframe src so the
4170
+ // page reloads even if the React-controlled src
4171
+ // didn't change (e.g. clicking the active tab,
4172
+ // or switching back to the same URL after SPA
4173
+ // navigation). This lets React.lazy retry any
4174
+ // previously-failed remote loads.
4175
+ if (mfIframeRef.current && url) {
4176
+ const target = url.endsWith("/")
4177
+ ? url
4178
+ : url + "/";
4179
+ mfIframeRef.current.src =
4180
+ target +
4181
+ moduleFederationPreviewPath.replace(
4182
+ /^\//,
4183
+ "",
4184
+ );
4185
+ }
4186
+ }}
3264
4187
  className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 ${
3265
4188
  mfPreviewApp === name
3266
4189
  ? "bg-slate-700 text-slate-100"
@@ -3366,6 +4289,11 @@ export default function CodeRunnerModal() {
3366
4289
  ref={nxIframeRef}
3367
4290
  src={nxSandboxUrl + reactPreviewPath}
3368
4291
  className="flex-1 min-h-0 w-full border-0 bg-white"
4292
+ style={
4293
+ isDraggingResize
4294
+ ? { pointerEvents: "none" }
4295
+ : undefined
4296
+ }
3369
4297
  title="Next.js Preview"
3370
4298
  onLoad={() => {
3371
4299
  try {
@@ -3390,6 +4318,11 @@ export default function CodeRunnerModal() {
3390
4318
  ref={mfIframeRef}
3391
4319
  src={moduleFederationPreviewUrl}
3392
4320
  className="flex-1 min-h-0 w-full border-0 bg-white"
4321
+ style={
4322
+ isDraggingResize
4323
+ ? { pointerEvents: "none" }
4324
+ : undefined
4325
+ }
3393
4326
  title="Webpack Module Federation Preview"
3394
4327
  />
3395
4328
  )}
@@ -3435,12 +4368,15 @@ export default function CodeRunnerModal() {
3435
4368
  startH: outputCollapsed ? 0 : sbxOutputH,
3436
4369
  };
3437
4370
  if (outputCollapsed) setOutputCollapsed(false);
4371
+ document.body.style.userSelect = "none";
4372
+ document.body.style.cursor = "row-resize";
4373
+ setIsDraggingResize(true);
3438
4374
  }}
3439
4375
  />
3440
4376
 
3441
4377
  {/* Sandbox output pane */}
3442
4378
  <div
3443
- className="bg-slate-950 flex flex-col overflow-hidden shrink-0 transition-[height]"
4379
+ className="bg-slate-950 flex flex-col overflow-hidden shrink-0"
3444
4380
  style={{ height: outputCollapsed ? 0 : sbxOutputH }}
3445
4381
  >
3446
4382
  {/* Tab bar */}
@@ -3474,6 +4410,20 @@ export default function CodeRunnerModal() {
3474
4410
  Console
3475
4411
  </button>
3476
4412
  )}
4413
+ {clientType === "module-federation" && (
4414
+ <button
4415
+ type="button"
4416
+ onClick={() => setSbxBottomTab("inspector")}
4417
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
4418
+ sbxBottomTab === "inspector"
4419
+ ? "border-amber-500 text-amber-300"
4420
+ : "border-transparent text-slate-500 hover:text-slate-300"
4421
+ }`}
4422
+ >
4423
+ <Eye className="w-3 h-3" />
4424
+ Inspector
4425
+ </button>
4426
+ )}
3477
4427
  <button
3478
4428
  type="button"
3479
4429
  onClick={() => {
@@ -3497,6 +4447,30 @@ export default function CodeRunnerModal() {
3497
4447
  {sbxBottomTab === "console" && mfConsoleRunning && (
3498
4448
  <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
3499
4449
  )}
4450
+ {sbxBottomTab === "inspector" && mfInspectorEvents.length > 0 && (
4451
+ <div className="flex items-center gap-1 mr-1">
4452
+ <button
4453
+ type="button"
4454
+ onClick={() =>
4455
+ navigator.clipboard.writeText(
4456
+ JSON.stringify(mfInspectorEvents, null, 2),
4457
+ )
4458
+ }
4459
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4460
+ title="Copy inspector events"
4461
+ >
4462
+ <Copy className="w-3 h-3" />
4463
+ </button>
4464
+ <button
4465
+ type="button"
4466
+ onClick={() => setMfInspectorEvents([])}
4467
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4468
+ title="Clear inspector events"
4469
+ >
4470
+ <Trash2 className="w-3 h-3" />
4471
+ </button>
4472
+ </div>
4473
+ )}
3500
4474
  {sbxBottomTab === "output" && sandboxOutput.length > 0 && (
3501
4475
  <div className="flex items-center gap-1 mr-1">
3502
4476
  <button
@@ -3616,6 +4590,1298 @@ export default function CodeRunnerModal() {
3616
4590
  </div>
3617
4591
  )}
3618
4592
 
4593
+ {sbxBottomTab === "inspector" && (
4594
+ <div className="flex-1 overflow-hidden flex flex-col text-[12px]">
4595
+ {/* Sub-view selector + meta bar */}
4596
+ <div className="shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-slate-800 bg-slate-900/80">
4597
+ <div className="flex items-center rounded overflow-hidden border border-slate-700 text-[10px]">
4598
+ {MF_INSPECTOR_VIEWS.map((view) => (
4599
+ <button
4600
+ key={view}
4601
+ type="button"
4602
+ onClick={() => setMfInspectorView(view)}
4603
+ title={MF_INSPECTOR_VIEW_COPY[view].description}
4604
+ className={`px-2.5 py-1 capitalize transition-colors ${
4605
+ mfInspectorView === view
4606
+ ? "bg-amber-500/20 text-amber-200"
4607
+ : "text-slate-500 hover:text-slate-300"
4608
+ }`}
4609
+ >
4610
+ {MF_INSPECTOR_VIEW_COPY[view].label}
4611
+ </button>
4612
+ ))}
4613
+ </div>
4614
+ <div className="flex items-center gap-2 ml-1 text-[10px] text-slate-500">
4615
+ <span>{mfInspectorEvents.length} timeline entries</span>
4616
+ <span>·</span>
4617
+ <span>
4618
+ {mfInspectorRuntimeCount} browser session
4619
+ {mfInspectorRuntimeCount !== 1 ? "s" : ""}
4620
+ </span>
4621
+ {mfInspectorSharedRemoteCount > 0 && (
4622
+ <>
4623
+ <span>·</span>
4624
+ <span className="text-emerald-400">
4625
+ {mfInspectorSharedRemoteCount} same React copy
4626
+ </span>
4627
+ </>
4628
+ )}
4629
+ </div>
4630
+ {mfPreviewApp !== "host" && mfInspectorView === "map" && (
4631
+ <span className="ml-auto text-[10px] text-amber-400/80 shrink-0 italic">
4632
+ Open the host preview to watch the host load remotes
4633
+ </span>
4634
+ )}
4635
+ </div>
4636
+
4637
+ <div
4638
+ className={`shrink-0 border-b border-slate-800 bg-slate-950/60 px-3 ${
4639
+ mfInspectorGuideCollapsed ? "py-2" : "py-3"
4640
+ }`}
4641
+ >
4642
+ <div className="rounded-xl border border-slate-800 bg-slate-900/60 px-3 py-3">
4643
+ <div className="flex items-start justify-between gap-3">
4644
+ <div className="min-w-0">
4645
+ <div className="text-[10px] uppercase tracking-wider text-sky-300/80 mb-1">
4646
+ Beginner guide
4647
+ </div>
4648
+ <div className="text-[13px] font-semibold text-slate-100">
4649
+ {mfInspectorViewCopy.title}
4650
+ </div>
4651
+ {mfInspectorGuideCollapsed && (
4652
+ <p className="mt-1 text-[10px] text-slate-500 leading-relaxed">
4653
+ Guide hidden. Expand it if you want the beginner
4654
+ explanation and summary cards back.
4655
+ </p>
4656
+ )}
4657
+ </div>
4658
+ <button
4659
+ type="button"
4660
+ onClick={() => setMfInspectorGuideCollapsed((v) => !v)}
4661
+ className="shrink-0 inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-950/70 px-2 py-1 text-[10px] text-slate-300 hover:text-slate-100 hover:border-slate-600 transition-colors"
4662
+ title={
4663
+ mfInspectorGuideCollapsed
4664
+ ? "Show beginner guide"
4665
+ : "Hide beginner guide"
4666
+ }
4667
+ >
4668
+ {mfInspectorGuideCollapsed ? (
4669
+ <ChevronDown className="w-3 h-3" />
4670
+ ) : (
4671
+ <ChevronUp className="w-3 h-3" />
4672
+ )}
4673
+ {mfInspectorGuideCollapsed ? "Show" : "Hide"}
4674
+ </button>
4675
+ </div>
4676
+ {!mfInspectorGuideCollapsed && (
4677
+ <>
4678
+ <p className="mt-1 text-[11px] text-slate-400 leading-relaxed max-w-3xl">
4679
+ {mfInspectorViewCopy.description}
4680
+ </p>
4681
+ <div className="mt-2 rounded-lg border border-slate-700/80 bg-slate-950/70 px-2.5 py-2 text-[10px] text-slate-300 leading-relaxed">
4682
+ <span className="font-semibold text-slate-100">
4683
+ What good looks like:
4684
+ </span>{" "}
4685
+ {mfPreviewApp !== "host" && mfInspectorView === "map"
4686
+ ? "You are previewing a remote directly right now. Switch back to the host page if you want to watch the host-to-remote lines move and update."
4687
+ : mfInspectorViewCopy.tip}
4688
+ </div>
4689
+ <div
4690
+ className="mt-3 grid gap-2"
4691
+ style={{
4692
+ gridTemplateColumns:
4693
+ "repeat(auto-fit, minmax(120px, 1fr))",
4694
+ }}
4695
+ >
4696
+ {mfInspectorSummaryCards.map((card) => (
4697
+ <div
4698
+ key={card.label}
4699
+ className="rounded-lg border border-slate-800 bg-slate-950/70 px-2.5 py-2"
4700
+ >
4701
+ <div className="text-[9px] uppercase tracking-wider text-slate-500">
4702
+ {card.label}
4703
+ </div>
4704
+ <div
4705
+ className={`mt-1 text-[13px] font-semibold ${card.toneClass}`}
4706
+ >
4707
+ {card.value}
4708
+ </div>
4709
+ <div className="mt-1 text-[10px] text-slate-500 leading-snug">
4710
+ {card.hint}
4711
+ </div>
4712
+ </div>
4713
+ ))}
4714
+ </div>
4715
+ </>
4716
+ )}
4717
+ </div>
4718
+ </div>
4719
+
4720
+ {/* View content */}
4721
+ <div className="flex-1 overflow-y-auto">
4722
+ {/* ── MAP VIEW ── */}
4723
+ {mfInspectorView === "map" && (
4724
+ <div>
4725
+ {mfInspectorEvents.length === 0 && !mfSandboxId ? (
4726
+ <div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 px-4 text-[11px]">
4727
+ <Eye className="w-6 h-6 opacity-30" />
4728
+ <p>
4729
+ Start the webpack lab, then open the host page to
4730
+ watch the host load remotes step by step.
4731
+ </p>
4732
+ </div>
4733
+ ) : (
4734
+ <>
4735
+ {/* SVG Topology diagram */}
4736
+ {(() => {
4737
+ const W = 500;
4738
+ const hostY = 46;
4739
+ const remoteY = 156;
4740
+ const nodeW = 96;
4741
+ const nodeH = 46;
4742
+ const svgH = 250;
4743
+ const hostBooted = mfInspectorBootMap.has("host");
4744
+ const hostData = mfInspectorBootMap.get("host");
4745
+ const hostPackageSummary =
4746
+ getMfInspectorAppSharedPackageSummary("host");
4747
+
4748
+ const getConnectionStatus = (
4749
+ remoteName: string,
4750
+ ):
4751
+ | "idle"
4752
+ | "loading"
4753
+ | "success-shared"
4754
+ | "success-different"
4755
+ | "error" => {
4756
+ const keys =
4757
+ mfTopologyRemoteKeysByApp.get(remoteName) ?? [];
4758
+ type IdentityEntry = {
4759
+ sameReact: boolean;
4760
+ sameReactDom: boolean;
4761
+ error: string | null;
4762
+ };
4763
+ type LoadEntry = {
4764
+ status: string;
4765
+ durationMs: number | null;
4766
+ };
4767
+ const identityResults = keys
4768
+ .map((k) => mfInspectorIdentityResultMap.get(k))
4769
+ .filter(Boolean) as IdentityEntry[];
4770
+ const loadResults = keys
4771
+ .map((k) => mfInspectorRemoteLoadMap.get(k))
4772
+ .filter(Boolean) as LoadEntry[];
4773
+ if (identityResults.some((r) => r.error))
4774
+ return "error";
4775
+ if (identityResults.length > 0)
4776
+ return identityResults.every(
4777
+ (r) => r.sameReact && r.sameReactDom,
4778
+ )
4779
+ ? "success-shared"
4780
+ : "success-different";
4781
+ if (loadResults.some((r) => r.status === "error"))
4782
+ return "error";
4783
+ if (
4784
+ loadResults.some(
4785
+ (r) =>
4786
+ r.status === "loading" ||
4787
+ r.status === "success",
4788
+ )
4789
+ )
4790
+ return "loading";
4791
+ // Only show "loading" (amber) if the host has
4792
+ // actually attempted to load this remote. A
4793
+ // standalone boot event from the remote's own
4794
+ // dev server doesn't mean the HOST is loading it.
4795
+ return "idle";
4796
+ };
4797
+
4798
+ const statusLineColors: Record<string, string> = {
4799
+ idle: "#334155",
4800
+ loading: "#d97706",
4801
+ "success-shared": "#10b981",
4802
+ "success-different": "#ef4444",
4803
+ error: "#ef4444",
4804
+ };
4805
+
4806
+ const displayRemotes =
4807
+ mfTopologyRemoteNames.length > 0
4808
+ ? mfTopologyRemoteNames
4809
+ : ["profile", "checkout"];
4810
+ const isPlaceholder =
4811
+ mfTopologyRemoteNames.length === 0;
4812
+
4813
+ const remotePositions = displayRemotes.map(
4814
+ (_, i) => {
4815
+ const step = W / (displayRemotes.length + 1);
4816
+ return step * (i + 1);
4817
+ },
4818
+ );
4819
+
4820
+ return (
4821
+ <div className="px-2 pt-3 pb-1">
4822
+ <svg
4823
+ viewBox={`0 0 ${W} ${svgH}`}
4824
+ width="100%"
4825
+ style={{ height: svgH, display: "block" }}
4826
+ >
4827
+ {/* Connection lines */}
4828
+ {displayRemotes.map((remoteName, i) => {
4829
+ const rx = remotePositions[i];
4830
+ const status = isPlaceholder
4831
+ ? "idle"
4832
+ : getConnectionStatus(remoteName);
4833
+ const lineColor =
4834
+ statusLineColors[status] ?? "#334155";
4835
+ const isDashed = status === "idle";
4836
+ const keys =
4837
+ mfTopologyRemoteKeysByApp.get(
4838
+ remoteName,
4839
+ ) ?? [];
4840
+ const loadResult = keys
4841
+ .map((k) =>
4842
+ mfInspectorRemoteLoadMap.get(k),
4843
+ )
4844
+ .find((r) => r?.durationMs != null);
4845
+ const durationLabel =
4846
+ loadResult?.durationMs != null
4847
+ ? `${loadResult.durationMs}ms`
4848
+ : "";
4849
+ const midX = (W / 2 + rx) / 2;
4850
+ const midY =
4851
+ (hostY +
4852
+ nodeH / 2 +
4853
+ remoteY -
4854
+ nodeH / 2) /
4855
+ 2;
4856
+ return (
4857
+ <g key={remoteName}>
4858
+ <line
4859
+ x1={W / 2}
4860
+ y1={hostY + nodeH / 2}
4861
+ x2={rx}
4862
+ y2={remoteY - nodeH / 2}
4863
+ stroke={lineColor}
4864
+ strokeWidth={isDashed ? 1.5 : 2}
4865
+ strokeDasharray={
4866
+ isDashed ? "5 4" : undefined
4867
+ }
4868
+ strokeLinecap="round"
4869
+ opacity={isPlaceholder ? 0.3 : 1}
4870
+ />
4871
+ {durationLabel && (
4872
+ <text
4873
+ x={midX}
4874
+ y={midY - 5}
4875
+ textAnchor="middle"
4876
+ fill={lineColor}
4877
+ fontSize={9}
4878
+ fontFamily="ui-monospace, monospace"
4879
+ >
4880
+ {durationLabel}
4881
+ </text>
4882
+ )}
4883
+ </g>
4884
+ );
4885
+ })}
4886
+
4887
+ {/* HOST node */}
4888
+ <rect
4889
+ x={W / 2 - nodeW / 2}
4890
+ y={hostY - nodeH / 2}
4891
+ width={nodeW}
4892
+ height={nodeH}
4893
+ rx={7}
4894
+ fill={hostBooted ? "#0c2a3d" : "#0f172a"}
4895
+ stroke={hostBooted ? "#38bdf8" : "#475569"}
4896
+ strokeWidth={hostBooted ? 2 : 1}
4897
+ opacity={isPlaceholder ? 0.5 : 1}
4898
+ />
4899
+ <text
4900
+ x={W / 2}
4901
+ y={hostY - 12}
4902
+ textAnchor="middle"
4903
+ fill={hostBooted ? "#e0f2fe" : "#94a3b8"}
4904
+ fontSize={11}
4905
+ fontWeight="700"
4906
+ fontFamily="ui-monospace, monospace"
4907
+ >
4908
+ host
4909
+ </text>
4910
+ {hostData?.reactVersion && (
4911
+ <text
4912
+ x={W / 2}
4913
+ y={hostY + 1}
4914
+ textAnchor="middle"
4915
+ fill="#7dd3fc"
4916
+ fontSize={8}
4917
+ fontFamily="ui-monospace, monospace"
4918
+ >
4919
+ {"⚛ React " + hostData.reactVersion}
4920
+ </text>
4921
+ )}
4922
+ {hostData?.reactDomVersion && (
4923
+ <text
4924
+ x={W / 2}
4925
+ y={hostY + 13}
4926
+ textAnchor="middle"
4927
+ fill="#7dd3fc"
4928
+ fontSize={8}
4929
+ fontFamily="ui-monospace, monospace"
4930
+ >
4931
+ {"▣ DOM " + hostData.reactDomVersion}
4932
+ </text>
4933
+ )}
4934
+ {hostPackageSummary.count > 0 && (
4935
+ <>
4936
+ <text
4937
+ x={W / 2}
4938
+ y={hostY + nodeH / 2 + 16}
4939
+ textAnchor="middle"
4940
+ fill="#cbd5e1"
4941
+ fontSize={8}
4942
+ fontFamily="ui-sans-serif, sans-serif"
4943
+ >
4944
+ {hostPackageSummary.countLabel}
4945
+ </text>
4946
+ <text
4947
+ x={W / 2}
4948
+ y={hostY + nodeH / 2 + 27}
4949
+ textAnchor="middle"
4950
+ fill="#64748b"
4951
+ fontSize={7.5}
4952
+ fontFamily="ui-monospace, monospace"
4953
+ >
4954
+ {hostPackageSummary.previewText}
4955
+ </text>
4956
+ </>
4957
+ )}
4958
+
4959
+ {/* REMOTE nodes */}
4960
+ {displayRemotes.map((remoteName, i) => {
4961
+ const rx = remotePositions[i];
4962
+ const status = isPlaceholder
4963
+ ? "idle"
4964
+ : getConnectionStatus(remoteName);
4965
+ const packageSummary =
4966
+ getMfInspectorAppSharedPackageSummary(
4967
+ remoteName,
4968
+ );
4969
+ const remoteData =
4970
+ mfInspectorBootMap.get(remoteName);
4971
+ const keys =
4972
+ mfTopologyRemoteKeysByApp.get(
4973
+ remoteName,
4974
+ ) ?? [];
4975
+ const identityResult = keys
4976
+ .map((k) =>
4977
+ mfInspectorIdentityResultMap.get(k),
4978
+ )
4979
+ .find((r) => r != null);
4980
+ const reactShared = identityResult
4981
+ ? identityResult.sameReact
4982
+ : null;
4983
+ const domShared = identityResult
4984
+ ? identityResult.sameReactDom
4985
+ : null;
4986
+ const bothShared =
4987
+ reactShared === true &&
4988
+ domShared === true;
4989
+ const eitherDifferent =
4990
+ reactShared === false ||
4991
+ domShared === false;
4992
+ const nodeFill = bothShared
4993
+ ? "#052e16"
4994
+ : eitherDifferent || status === "error"
4995
+ ? "#450a0a"
4996
+ : status === "loading"
4997
+ ? "#1c1917"
4998
+ : "#0f172a";
4999
+ const nodeStroke = bothShared
5000
+ ? "#16a34a"
5001
+ : eitherDifferent || status === "error"
5002
+ ? "#dc2626"
5003
+ : status === "loading"
5004
+ ? "#d97706"
5005
+ : remoteData
5006
+ ? "#818cf8"
5007
+ : "#334155";
5008
+ // Build per-library status label
5009
+ const reactLabel =
5010
+ reactShared === true
5011
+ ? "\u2713 React"
5012
+ : reactShared === false
5013
+ ? "\u2717 React"
5014
+ : status === "loading"
5015
+ ? "React\u2026"
5016
+ : "";
5017
+ const domLabel =
5018
+ domShared === true
5019
+ ? "\u2713 DOM"
5020
+ : domShared === false
5021
+ ? "\u2717 DOM"
5022
+ : status === "loading"
5023
+ ? "DOM\u2026"
5024
+ : "";
5025
+ const statusLabel =
5026
+ status === "error"
5027
+ ? "\u2717 error"
5028
+ : [reactLabel, domLabel]
5029
+ .filter(Boolean)
5030
+ .join(" ");
5031
+ const reactLabelColor =
5032
+ reactShared === true
5033
+ ? "#4ade80"
5034
+ : reactShared === false
5035
+ ? "#f87171"
5036
+ : "#fbbf24";
5037
+ const domLabelColor =
5038
+ domShared === true
5039
+ ? "#4ade80"
5040
+ : domShared === false
5041
+ ? "#f87171"
5042
+ : "#fbbf24";
5043
+ const reactVer =
5044
+ remoteData?.reactVersion ??
5045
+ identityResult?.reactVersion;
5046
+ const reactDomVer =
5047
+ remoteData?.reactDomVersion ??
5048
+ identityResult?.reactDomVersion;
5049
+
5050
+ return (
5051
+ <g
5052
+ key={remoteName}
5053
+ opacity={isPlaceholder ? 0.35 : 1}
5054
+ >
5055
+ <rect
5056
+ x={rx - nodeW / 2}
5057
+ y={remoteY - nodeH / 2}
5058
+ width={nodeW}
5059
+ height={nodeH}
5060
+ rx={7}
5061
+ fill={nodeFill}
5062
+ stroke={nodeStroke}
5063
+ strokeWidth={
5064
+ status !== "idle" ? 2 : 1
5065
+ }
5066
+ />
5067
+ <text
5068
+ x={rx}
5069
+ y={remoteY - 13}
5070
+ textAnchor="middle"
5071
+ fill="#e2e8f0"
5072
+ fontSize={11}
5073
+ fontWeight="700"
5074
+ fontFamily="ui-monospace, monospace"
5075
+ >
5076
+ {remoteName}
5077
+ </text>
5078
+ {/* React row */}
5079
+ <text
5080
+ x={rx - nodeW / 2 + 8}
5081
+ y={remoteY + 2}
5082
+ fill={
5083
+ reactShared === true
5084
+ ? "#4ade80"
5085
+ : reactShared === false
5086
+ ? "#f87171"
5087
+ : "#64748b"
5088
+ }
5089
+ fontSize={8}
5090
+ fontFamily="ui-monospace, monospace"
5091
+ >
5092
+
5093
+ </text>
5094
+ <text
5095
+ x={rx - nodeW / 2 + 19}
5096
+ y={remoteY + 2}
5097
+ fill="#94a3b8"
5098
+ fontSize={8}
5099
+ fontFamily="ui-monospace, monospace"
5100
+ >
5101
+ {reactVer
5102
+ ? "React " + reactVer
5103
+ : "React"}
5104
+ </text>
5105
+ {reactShared !== null && (
5106
+ <text
5107
+ x={rx + nodeW / 2 - 6}
5108
+ y={remoteY + 2}
5109
+ textAnchor="end"
5110
+ fill={
5111
+ reactShared
5112
+ ? "#4ade80"
5113
+ : "#f87171"
5114
+ }
5115
+ fontSize={8}
5116
+ fontFamily="ui-monospace, monospace"
5117
+ >
5118
+ {reactShared ? "\u2713" : "\u2717"}
5119
+ </text>
5120
+ )}
5121
+ {/* ReactDOM row */}
5122
+ <text
5123
+ x={rx - nodeW / 2 + 8}
5124
+ y={remoteY + 14}
5125
+ fill={
5126
+ domShared === true
5127
+ ? "#4ade80"
5128
+ : domShared === false
5129
+ ? "#f87171"
5130
+ : "#64748b"
5131
+ }
5132
+ fontSize={8}
5133
+ fontFamily="ui-monospace, monospace"
5134
+ >
5135
+
5136
+ </text>
5137
+ <text
5138
+ x={rx - nodeW / 2 + 19}
5139
+ y={remoteY + 14}
5140
+ fill="#94a3b8"
5141
+ fontSize={8}
5142
+ fontFamily="ui-monospace, monospace"
5143
+ >
5144
+ {reactDomVer
5145
+ ? "DOM " + reactDomVer
5146
+ : "ReactDOM"}
5147
+ </text>
5148
+ {domShared !== null && (
5149
+ <text
5150
+ x={rx + nodeW / 2 - 6}
5151
+ y={remoteY + 14}
5152
+ textAnchor="end"
5153
+ fill={
5154
+ domShared ? "#4ade80" : "#f87171"
5155
+ }
5156
+ fontSize={8}
5157
+ fontFamily="ui-monospace, monospace"
5158
+ >
5159
+ {domShared ? "\u2713" : "\u2717"}
5160
+ </text>
5161
+ )}
5162
+ {/* Status label below node — show per-lib check marks */}
5163
+ {(statusLabel ||
5164
+ status === "loading") && (
5165
+ <g>
5166
+ {reactLabel && (
5167
+ <text
5168
+ x={rx - 20}
5169
+ y={remoteY + nodeH / 2 + 14}
5170
+ textAnchor="middle"
5171
+ fill={reactLabelColor}
5172
+ fontSize={8.5}
5173
+ fontFamily="ui-monospace, monospace"
5174
+ >
5175
+ {reactLabel}
5176
+ </text>
5177
+ )}
5178
+ {domLabel && (
5179
+ <text
5180
+ x={rx + 22}
5181
+ y={remoteY + nodeH / 2 + 14}
5182
+ textAnchor="middle"
5183
+ fill={domLabelColor}
5184
+ fontSize={8.5}
5185
+ fontFamily="ui-monospace, monospace"
5186
+ >
5187
+ {domLabel}
5188
+ </text>
5189
+ )}
5190
+ {status === "error" && (
5191
+ <text
5192
+ x={rx}
5193
+ y={remoteY + nodeH / 2 + 14}
5194
+ textAnchor="middle"
5195
+ fill="#f87171"
5196
+ fontSize={8.5}
5197
+ fontFamily="ui-monospace, monospace"
5198
+ >
5199
+ ✕ error
5200
+ </text>
5201
+ )}
5202
+ </g>
5203
+ )}
5204
+ {packageSummary.count > 0 && (
5205
+ <g>
5206
+ <text
5207
+ x={rx}
5208
+ y={remoteY + nodeH / 2 + 26}
5209
+ textAnchor="middle"
5210
+ fill="#cbd5e1"
5211
+ fontSize={8}
5212
+ fontFamily="ui-sans-serif, sans-serif"
5213
+ >
5214
+ {packageSummary.countLabel}
5215
+ </text>
5216
+ <text
5217
+ x={rx}
5218
+ y={remoteY + nodeH / 2 + 37}
5219
+ textAnchor="middle"
5220
+ fill="#64748b"
5221
+ fontSize={7.5}
5222
+ fontFamily="ui-monospace, monospace"
5223
+ >
5224
+ {packageSummary.previewText}
5225
+ </text>
5226
+ </g>
5227
+ )}
5228
+ </g>
5229
+ );
5230
+ })}
5231
+
5232
+ {/* Legend strip */}
5233
+ {(
5234
+ [
5235
+ {
5236
+ color: "#334155",
5237
+ label: "not asked yet",
5238
+ dash: true,
5239
+ },
5240
+ {
5241
+ color: "#d97706",
5242
+ label: "loading / checking",
5243
+ dash: false,
5244
+ },
5245
+ {
5246
+ color: "#10b981",
5247
+ label: "same React copy",
5248
+ dash: false,
5249
+ },
5250
+ {
5251
+ color: "#ef4444",
5252
+ label: "different / error",
5253
+ dash: false,
5254
+ },
5255
+ ] as Array<{
5256
+ color: string;
5257
+ label: string;
5258
+ dash: boolean;
5259
+ }>
5260
+ ).map(
5261
+ ({ color, label, dash: isDash }, li) => (
5262
+ <g
5263
+ key={li}
5264
+ transform={`translate(${li * 126}, ${svgH - 11})`}
5265
+ >
5266
+ <line
5267
+ x1={0}
5268
+ y1={0}
5269
+ x2={14}
5270
+ y2={0}
5271
+ stroke={color}
5272
+ strokeWidth={2}
5273
+ strokeDasharray={
5274
+ isDash ? "4 3" : undefined
5275
+ }
5276
+ strokeLinecap="round"
5277
+ />
5278
+ <text
5279
+ x={18}
5280
+ y={1}
5281
+ fill="#475569"
5282
+ fontSize={8.5}
5283
+ fontFamily="ui-sans-serif, sans-serif"
5284
+ dominantBaseline="middle"
5285
+ >
5286
+ {label}
5287
+ </text>
5288
+ </g>
5289
+ ),
5290
+ )}
5291
+ </svg>
5292
+ </div>
5293
+ );
5294
+ })()}
5295
+
5296
+ {/* Instance-sharing truth table */}
5297
+ {mfInspectorRemoteTableKeys.length > 0 && (
5298
+ <div className="px-3 pb-3">
5299
+ <div className="text-[10px] uppercase tracking-wider text-slate-500 mb-2">
5300
+ Did the host and remote reuse the same React
5301
+ copy?
5302
+ </div>
5303
+ <div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
5304
+ "Same copy" is the healthy result for React and
5305
+ ReactDOM. "Different copy" often leads to hook,
5306
+ context, or shared-state bugs. The package list
5307
+ column reflects the current app package.json
5308
+ files.
5309
+ </div>
5310
+ <div className="rounded-lg border border-slate-800 overflow-hidden">
5311
+ <table className="w-full text-[11px]">
5312
+ <thead>
5313
+ <tr className="border-b border-slate-800 bg-slate-900/80">
5314
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5315
+ Remote module
5316
+ </th>
5317
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5318
+ React copy
5319
+ </th>
5320
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5321
+ DOM copy
5322
+ </th>
5323
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5324
+ Current shared packages
5325
+ </th>
5326
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5327
+ Version seen
5328
+ </th>
5329
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5330
+ Load time
5331
+ </th>
5332
+ </tr>
5333
+ </thead>
5334
+ <tbody>
5335
+ {mfInspectorRemoteTableKeys.map(
5336
+ (remoteKey) => {
5337
+ const result =
5338
+ mfInspectorIdentityResultMap.get(
5339
+ remoteKey,
5340
+ ) ?? null;
5341
+ const remoteAppName =
5342
+ result?.remoteApp ??
5343
+ remoteKey.split("/")[0] ??
5344
+ "unknown";
5345
+ const loadInfo =
5346
+ mfInspectorRemoteLoadMap.get(
5347
+ remoteKey,
5348
+ );
5349
+ return (
5350
+ <tr
5351
+ key={remoteKey}
5352
+ className="border-b border-slate-800/50 last:border-0"
5353
+ >
5354
+ <td className="px-3 py-2 font-mono text-slate-200">
5355
+ {remoteKey}
5356
+ </td>
5357
+ <td className="px-3 py-2">
5358
+ {renderMfInspectorTruthCell(
5359
+ result?.sameReact ?? null,
5360
+ result?.error ?? null,
5361
+ loadInfo?.status ?? null,
5362
+ )}
5363
+ </td>
5364
+ <td className="px-3 py-2">
5365
+ {renderMfInspectorTruthCell(
5366
+ result?.sameReactDom ?? null,
5367
+ result?.error ?? null,
5368
+ loadInfo?.status ?? null,
5369
+ )}
5370
+ </td>
5371
+ <td className="px-3 py-2 align-top">
5372
+ {renderMfInspectorPackageBadges(
5373
+ remoteAppName,
5374
+ )}
5375
+ </td>
5376
+ <td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
5377
+ {result?.reactVersion ??
5378
+ (loadInfo?.status ===
5379
+ "loading" ||
5380
+ loadInfo?.status === "success"
5381
+ ? "checking..."
5382
+ : "\u2014")}
5383
+ </td>
5384
+ <td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
5385
+ {loadInfo?.durationMs != null
5386
+ ? `${loadInfo.durationMs}ms`
5387
+ : loadInfo?.status === "loading"
5388
+ ? "loading..."
5389
+ : "\u2014"}
5390
+ </td>
5391
+ </tr>
5392
+ );
5393
+ },
5394
+ )}
5395
+ </tbody>
5396
+ </table>
5397
+ </div>
5398
+ </div>
5399
+ )}
5400
+
5401
+ {/* Declared config cards */}
5402
+ {mfInspectorDeclaredConfigs.length > 0 && (
5403
+ <div className="px-3 pb-3">
5404
+ <div className="text-[10px] uppercase tracking-wider text-slate-500 mb-2">
5405
+ What each app promised in webpack config
5406
+ </div>
5407
+ <div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
5408
+ This is the static setup from config files,
5409
+ before the browser proves anything at runtime.
5410
+ </div>
5411
+ <div
5412
+ className="grid gap-2"
5413
+ style={{
5414
+ gridTemplateColumns:
5415
+ "repeat(auto-fill, minmax(160px, 1fr))",
5416
+ }}
5417
+ >
5418
+ {mfInspectorDeclaredConfigs.map(
5419
+ ({ app, declaredConfig }) => {
5420
+ const remotes = Object.keys(
5421
+ asInspectorRecord(declaredConfig.remotes),
5422
+ );
5423
+ const exposes = asInspectorStringArray(
5424
+ declaredConfig.exposes,
5425
+ );
5426
+ const sharedPkgs = Object.keys(
5427
+ asInspectorRecord(declaredConfig.shared),
5428
+ );
5429
+ return (
5430
+ <div
5431
+ key={app}
5432
+ className="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2.5 text-[11px]"
5433
+ >
5434
+ <div className="font-semibold text-slate-100 font-mono mb-2">
5435
+ {app}
5436
+ </div>
5437
+ {remotes.length > 0 && (
5438
+ <div className="mb-1.5">
5439
+ <div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
5440
+ can call these remotes
5441
+ </div>
5442
+ <div className="flex flex-wrap gap-1">
5443
+ {remotes.map((r) => (
5444
+ <span
5445
+ key={r}
5446
+ className="rounded bg-cyan-500/10 border border-cyan-500/20 px-1.5 py-0.5 text-cyan-300 text-[10px] font-mono"
5447
+ >
5448
+ {r}
5449
+ </span>
5450
+ ))}
5451
+ </div>
5452
+ </div>
5453
+ )}
5454
+ {exposes.length > 0 && (
5455
+ <div className="mb-1.5">
5456
+ <div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
5457
+ offers these modules
5458
+ </div>
5459
+ <div className="flex flex-wrap gap-1">
5460
+ {exposes.map((e) => (
5461
+ <span
5462
+ key={e}
5463
+ className="rounded bg-violet-500/10 border border-violet-500/20 px-1.5 py-0.5 text-violet-300 text-[10px] font-mono"
5464
+ >
5465
+ {e}
5466
+ </span>
5467
+ ))}
5468
+ </div>
5469
+ </div>
5470
+ )}
5471
+ {sharedPkgs.length > 0 && (
5472
+ <div>
5473
+ <div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
5474
+ wants to share (
5475
+ {sharedPkgs.length})
5476
+ </div>
5477
+ <div className="flex flex-wrap gap-1">
5478
+ {sharedPkgs.map((p) => {
5479
+ const cfg = asInspectorRecord(
5480
+ asInspectorRecord(
5481
+ declaredConfig.shared,
5482
+ )[p],
5483
+ );
5484
+ const singleton =
5485
+ cfg.singleton === true;
5486
+ return (
5487
+ <span
5488
+ key={p}
5489
+ className={`rounded border px-1.5 py-0.5 text-[10px] font-mono ${
5490
+ singleton
5491
+ ? "border-amber-500/30 bg-amber-500/10 text-amber-200"
5492
+ : "border-slate-700 bg-slate-800/50 text-slate-300"
5493
+ }`}
5494
+ >
5495
+ {p}
5496
+ {singleton
5497
+ ? " \u00b7 one copy"
5498
+ : ""}
5499
+ </span>
5500
+ );
5501
+ })}
5502
+ </div>
5503
+ </div>
5504
+ )}
5505
+ </div>
5506
+ );
5507
+ },
5508
+ )}
5509
+ </div>
5510
+ </div>
5511
+ )}
5512
+ </>
5513
+ )}
5514
+ </div>
5515
+ )}
5516
+
5517
+ {/* ── SHARES VIEW ── */}
5518
+ {mfInspectorView === "shares" && (
5519
+ <div className="px-3 py-3">
5520
+ {!mfShareScopeForMatrix ? (
5521
+ <div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 text-[11px]">
5522
+ <div className="opacity-20 text-3xl select-none">
5523
+
5524
+ </div>
5525
+ <p>
5526
+ Open the host preview first to populate webpack's
5527
+ live shared-package list.
5528
+ </p>
5529
+ <p className="text-[10px] text-slate-600">
5530
+ The inspector reads webpack&apos;s live{" "}
5531
+ <code className="font-mono">
5532
+ __webpack_share_scopes__
5533
+ </code>{" "}
5534
+ registry at runtime.
5535
+ </p>
5536
+ </div>
5537
+ ) : (
5538
+ <>
5539
+ <div className="rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 mb-3">
5540
+ <div className="text-[12px] font-semibold text-slate-100">
5541
+ Shared package picture
5542
+ </div>
5543
+ <p className="mt-1 text-[11px] text-slate-400 leading-relaxed">
5544
+ This tab merges two sources: the packages found in
5545
+ the current app package.json files in the editor,
5546
+ and the packages webpack has actually registered
5547
+ in its live runtime share scope.
5548
+ </p>
5549
+ <p className="mt-1 text-[10px] text-slate-500">
5550
+ Snapshot source:{" "}
5551
+ <span className="text-amber-300 font-mono">
5552
+ {mfShareScopeForMatrix.app}
5553
+ </span>{" "}
5554
+ at{" "}
5555
+ {formatInspectorTimestamp(
5556
+ mfShareScopeForMatrix.timestamp,
5557
+ )}
5558
+ </p>
5559
+ </div>
5560
+ {mfInspectorShareScopeNames.map((scopeName) => {
5561
+ const scopeValue =
5562
+ scopeName === "default"
5563
+ ? asInspectorRecord(
5564
+ mfShareScopeForMatrix.shareScopes.default,
5565
+ )
5566
+ : asInspectorRecord(
5567
+ mfShareScopeForMatrix.shareScopes[
5568
+ scopeName
5569
+ ],
5570
+ );
5571
+ const packageNames = Array.from(
5572
+ new Set([
5573
+ ...Object.keys(scopeValue),
5574
+ ...(scopeName === "default"
5575
+ ? Array.from(
5576
+ mfInspectorDeclaredSharedPackageMap.keys(),
5577
+ )
5578
+ : []),
5579
+ ]),
5580
+ ).sort((left, right) => left.localeCompare(right));
5581
+
5582
+ if (packageNames.length === 0) return null;
5583
+
5584
+ return (
5585
+ <div key={scopeName} className="mb-4">
5586
+ <div className="text-[10px] text-slate-500 uppercase tracking-wider mb-2 font-mono">
5587
+ sharing group: {scopeName}
5588
+ </div>
5589
+ <div className="rounded-lg border border-slate-800 overflow-hidden">
5590
+ <table className="w-full text-[11px]">
5591
+ <thead>
5592
+ <tr className="bg-slate-900/80 border-b border-slate-800">
5593
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-32">
5594
+ Package
5595
+ </th>
5596
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-56">
5597
+ Declared by apps
5598
+ </th>
5599
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5600
+ Versions webpack can reuse
5601
+ </th>
5602
+ </tr>
5603
+ </thead>
5604
+ <tbody>
5605
+ {packageNames.map((pkgName) => {
5606
+ const versionValue =
5607
+ scopeValue[pkgName];
5608
+ const versionEntries = Array.isArray(
5609
+ versionValue,
5610
+ )
5611
+ ? versionValue
5612
+ : [];
5613
+ const declaredInfo =
5614
+ mfInspectorDeclaredSharedPackageMap.get(
5615
+ pkgName,
5616
+ ) ?? null;
5617
+ const declaredSingleton =
5618
+ declaredInfo?.singletonPreferred ===
5619
+ true;
5620
+ const declaredVersionSummary =
5621
+ declaredInfo?.requiredVersions
5622
+ .filter(
5623
+ ({ version }) => version !== null,
5624
+ )
5625
+ .map(({ app, version }) =>
5626
+ version === false
5627
+ ? `${app}: any`
5628
+ : `${app}: ${version}`,
5629
+ ) ?? [];
5630
+ const loadedCount =
5631
+ versionEntries.filter(
5632
+ (ve) =>
5633
+ asInspectorRecord(ve).loaded ===
5634
+ true,
5635
+ ).length;
5636
+ return (
5637
+ <tr
5638
+ key={pkgName}
5639
+ className="border-b border-slate-800/50 last:border-0"
5640
+ >
5641
+ <td className="px-3 py-2.5 align-top">
5642
+ <div className="font-mono text-slate-100 font-medium">
5643
+ {pkgName}
5644
+ </div>
5645
+ <div className="flex flex-wrap gap-1 mt-1">
5646
+ {declaredSingleton && (
5647
+ <span className="rounded border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 text-[9px] text-amber-300">
5648
+ one copy preferred
5649
+ </span>
5650
+ )}
5651
+ {declaredInfo ? (
5652
+ <span className="rounded border border-cyan-500/30 bg-cyan-500/10 px-1 py-0.5 text-[9px] text-cyan-300">
5653
+ from current package.json
5654
+ </span>
5655
+ ) : (
5656
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-500">
5657
+ runtime only
5658
+ </span>
5659
+ )}
5660
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-400">
5661
+ {loadedCount}/
5662
+ {versionEntries.length}{" "}
5663
+ already used
5664
+ </span>
5665
+ </div>
5666
+ </td>
5667
+ <td className="px-3 py-2.5 align-top">
5668
+ {declaredInfo ? (
5669
+ <div>
5670
+ <div className="flex flex-wrap gap-1">
5671
+ {declaredInfo.apps.map(
5672
+ (appName) => (
5673
+ <span
5674
+ key={appName}
5675
+ className="rounded border border-slate-700 bg-slate-800/50 px-1.5 py-0.5 text-[9px] text-slate-300 font-mono"
5676
+ >
5677
+ {appName}
5678
+ </span>
5679
+ ),
5680
+ )}
5681
+ </div>
5682
+ {declaredVersionSummary.length >
5683
+ 0 && (
5684
+ <div className="mt-1 text-[9px] text-slate-500 leading-relaxed">
5685
+ required versions:{" "}
5686
+ {declaredVersionSummary.join(
5687
+ ", ",
5688
+ )}
5689
+ </div>
5690
+ )}
5691
+ </div>
5692
+ ) : (
5693
+ <span className="text-slate-600">
5694
+ not declared in current app
5695
+ config
5696
+ </span>
5697
+ )}
5698
+ </td>
5699
+ <td className="px-3 py-2.5">
5700
+ <div className="flex flex-wrap gap-2">
5701
+ {versionEntries.length === 0 ? (
5702
+ declaredInfo ? (
5703
+ <div className="rounded-lg border border-slate-700 bg-slate-800/40 px-2 py-1.5 text-[10px] text-slate-400 leading-relaxed max-w-[22rem]">
5704
+ Declared for sharing, but
5705
+ webpack has not put it in
5706
+ the live runtime registry
5707
+ yet.
5708
+ </div>
5709
+ ) : (
5710
+ <span className="text-slate-600">
5711
+ none yet
5712
+ </span>
5713
+ )
5714
+ ) : (
5715
+ versionEntries.map(
5716
+ (ve, vi) => {
5717
+ const vr =
5718
+ asInspectorRecord(ve);
5719
+ const version =
5720
+ typeof vr.version ===
5721
+ "string"
5722
+ ? vr.version
5723
+ : "?";
5724
+ const from =
5725
+ typeof vr.from ===
5726
+ "string"
5727
+ ? vr.from
5728
+ : null;
5729
+ const loaded =
5730
+ vr.loaded === true;
5731
+ const eager =
5732
+ vr.eager === true;
5733
+ return (
5734
+ <div
5735
+ key={vi}
5736
+ className={`rounded-lg border px-2 py-1.5 font-mono text-[10px] ${
5737
+ loaded
5738
+ ? "border-emerald-500/30 bg-emerald-500/5"
5739
+ : "border-slate-700 bg-slate-800/40"
5740
+ }`}
5741
+ >
5742
+ <div
5743
+ className={`font-semibold ${loaded ? "text-emerald-200" : "text-slate-400"}`}
5744
+ >
5745
+ {version}
5746
+ </div>
5747
+ {from && (
5748
+ <div className="text-slate-500 mt-0.5 text-[9px]">
5749
+ registered by{" "}
5750
+ {from}
5751
+ </div>
5752
+ )}
5753
+ <div className="flex gap-1 mt-1">
5754
+ <span
5755
+ className={`rounded px-1 py-0.5 text-[8px] ${
5756
+ loaded
5757
+ ? "bg-emerald-500/20 text-emerald-300"
5758
+ : "bg-slate-700 text-slate-500"
5759
+ }`}
5760
+ >
5761
+ {loaded
5762
+ ? "in use"
5763
+ : "registered"}
5764
+ </span>
5765
+ <span
5766
+ className={`rounded px-1 py-0.5 text-[8px] ${
5767
+ eager
5768
+ ? "bg-amber-500/20 text-amber-300"
5769
+ : "bg-slate-800 text-slate-600"
5770
+ }`}
5771
+ >
5772
+ {eager
5773
+ ? "load now"
5774
+ : "load later"}
5775
+ </span>
5776
+ </div>
5777
+ </div>
5778
+ );
5779
+ },
5780
+ )
5781
+ )}
5782
+ </div>
5783
+ </td>
5784
+ </tr>
5785
+ );
5786
+ })}
5787
+ </tbody>
5788
+ </table>
5789
+ </div>
5790
+ </div>
5791
+ );
5792
+ })}
5793
+ {typeof mfShareScopeForMatrix.shareScopes.__error ===
5794
+ "string" && (
5795
+ <div className="rounded border border-red-500/20 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
5796
+ {mfShareScopeForMatrix.shareScopes.__error}
5797
+ </div>
5798
+ )}
5799
+ </>
5800
+ )}
5801
+ </div>
5802
+ )}
5803
+
5804
+ {/* ── EVENTS VIEW ── */}
5805
+ {mfInspectorView === "events" && (
5806
+ <div className="py-2 px-2">
5807
+ {mfInspectorRecentEvents.length === 0 ? (
5808
+ <div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 text-[11px]">
5809
+ <Terminal className="w-5 h-5 opacity-30" />
5810
+ <p>
5811
+ No browser events yet. Start the webpack lab, then
5812
+ open the host page.
5813
+ </p>
5814
+ </div>
5815
+ ) : (
5816
+ <div className="space-y-2">
5817
+ <div className="px-1 text-[10px] text-slate-500 leading-relaxed">
5818
+ Newest first. Read each row as: who said something,
5819
+ what happened, and why that step matters.
5820
+ </div>
5821
+ {mfInspectorRecentEvents.map((event) => {
5822
+ const leftBorderColor = event.kind.includes("error")
5823
+ ? "border-l-red-500"
5824
+ : event.kind === "identity-check"
5825
+ ? "border-l-emerald-500"
5826
+ : event.kind === "remote-load-success"
5827
+ ? "border-l-cyan-500"
5828
+ : event.kind === "share-snapshot"
5829
+ ? "border-l-amber-500"
5830
+ : event.kind === "runtime-boot"
5831
+ ? "border-l-sky-500"
5832
+ : "border-l-slate-700";
5833
+ const appColor =
5834
+ event.app === "host"
5835
+ ? "text-sky-400"
5836
+ : "text-violet-400";
5837
+ return (
5838
+ <div
5839
+ key={`${event.runtimeId}-${event.timestamp}-${event.kind}`}
5840
+ className={`flex gap-3 rounded-lg border border-slate-800 bg-slate-900/40 px-3 py-2 border-l-2 hover:bg-slate-800/30 transition-colors ${leftBorderColor}`}
5841
+ >
5842
+ <div className="shrink-0 text-[10px] font-mono text-slate-600 w-16 pt-0.5">
5843
+ {formatInspectorTimestamp(event.timestamp)}
5844
+ </div>
5845
+ <div className="min-w-0 flex-1">
5846
+ <div className="flex flex-wrap items-center gap-1.5 text-[10px]">
5847
+ <span
5848
+ className={`font-mono font-semibold ${appColor}`}
5849
+ >
5850
+ {formatMfInspectorAppLabel(event.app)}
5851
+ </span>
5852
+ <span
5853
+ title={`Raw event name: ${event.kind}`}
5854
+ className={`rounded px-1.5 py-0.5 ${getMfInspectorBadgeClass(event.kind)}`}
5855
+ >
5856
+ {getMfInspectorFriendlyKindLabel(
5857
+ event.kind,
5858
+ )}
5859
+ </span>
5860
+ {event.route && event.route !== "/" && (
5861
+ <span className="font-mono text-slate-600">
5862
+ on {event.route}
5863
+ </span>
5864
+ )}
5865
+ </div>
5866
+ <div className="mt-1 text-[11px] text-slate-300 leading-relaxed">
5867
+ {describeMfInspectorEvent(event)}
5868
+ </div>
5869
+ <div className="mt-1 text-[10px] text-slate-500 leading-relaxed">
5870
+ Why it matters:{" "}
5871
+ {explainMfInspectorEvent(event)}
5872
+ </div>
5873
+ </div>
5874
+ </div>
5875
+ );
5876
+ })}
5877
+ </div>
5878
+ )}
5879
+ </div>
5880
+ )}
5881
+ </div>
5882
+ </div>
5883
+ )}
5884
+
3619
5885
  {sbxBottomTab === "console" && (
3620
5886
  <div className="flex-1 min-h-0 flex flex-col">
3621
5887
  <div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">