@volter/editor-game 0.5.65
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.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network inspector (W3b, F9) — the full replicated-state introspection
|
|
3
|
+
* surface over `SystemAdapters.NetworkingAdapter`:
|
|
4
|
+
*
|
|
5
|
+
* - replicated-state TREE (live `getStateSnapshot()`; collapsed nodes mount
|
|
6
|
+
* no children, so a collapsed subtree costs one row per poll);
|
|
7
|
+
* - MESSAGE LOG (`messageEvents` seq-fenced ring → `MessageLogModel`;
|
|
8
|
+
* pausable, filterable by type);
|
|
9
|
+
* - rate SPARKLINES (`getRates()` sampled once per second into
|
|
10
|
+
* `RateHistory`; inline SVG polylines);
|
|
11
|
+
* - latency/loss CONDITIONER (`get/setConditioning` — applied at the
|
|
12
|
+
* adapter seam, M3).
|
|
13
|
+
*
|
|
14
|
+
* Degradation ladder rendered honestly (W3a contract, first system-adapter
|
|
15
|
+
* consumer): no adapter → the register-an-adapter notice; an adapter missing
|
|
16
|
+
* an optional capability → that section says "not provided by this adapter";
|
|
17
|
+
* nothing is ever fabricated. Editor code never imports Colyseus here — only
|
|
18
|
+
* the `NetworkingAdapter` interface (net-import-ban).
|
|
19
|
+
*
|
|
20
|
+
* Mounted as the `network` workspace utility (drawer tab beside
|
|
21
|
+
* Console/Profiler, `core-utilities.tsx`), available only while a
|
|
22
|
+
* real `NetworkingAdapter` is registered. Every caller that wants network
|
|
23
|
+
* diagnostics reveals that ONE utility.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
27
|
+
import {
|
|
28
|
+
Button,
|
|
29
|
+
DisclosureIcon,
|
|
30
|
+
fontSizeVar,
|
|
31
|
+
NumberInput,
|
|
32
|
+
spaceVar,
|
|
33
|
+
TextInput,
|
|
34
|
+
themeVars,
|
|
35
|
+
} from '@volter/editor-sdk/widgets';
|
|
36
|
+
import type {
|
|
37
|
+
ConnectionState,
|
|
38
|
+
NetConditioning,
|
|
39
|
+
NetMessageEvent,
|
|
40
|
+
NetworkingAdapter,
|
|
41
|
+
} from '@volter/editor-project/adapter';
|
|
42
|
+
import { memo, useEffect, useReducer, useRef, useState } from 'react';
|
|
43
|
+
import {
|
|
44
|
+
deriveNetworkCapabilities,
|
|
45
|
+
deriveNetworkView,
|
|
46
|
+
filterMessages,
|
|
47
|
+
MessageLogModel,
|
|
48
|
+
type NetworkCapabilityMap,
|
|
49
|
+
type NetworkView,
|
|
50
|
+
RateHistory,
|
|
51
|
+
type RateSeries,
|
|
52
|
+
sparklinePoints,
|
|
53
|
+
} from './network-inspector-model';
|
|
54
|
+
|
|
55
|
+
const STATE_COLORS: Record<ConnectionState, string> = {
|
|
56
|
+
disconnected: themeVars.content.muted,
|
|
57
|
+
connecting: themeVars.semantic.warning,
|
|
58
|
+
connected: themeVars.semantic.success,
|
|
59
|
+
error: themeVars.semantic.danger,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const MONO: React.CSSProperties = {
|
|
63
|
+
fontFamily: themeVars.typography.mono,
|
|
64
|
+
fontSize: fontSizeVar.base,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function AbsentNote({ children, testId }: { children: React.ReactNode; testId?: string }) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-testid={testId}
|
|
71
|
+
style={{
|
|
72
|
+
padding: spaceVar[4],
|
|
73
|
+
color: themeVars.content.muted,
|
|
74
|
+
fontSize: fontSizeVar.base,
|
|
75
|
+
fontStyle: 'italic',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Replicated-state tree ------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function isBranch(value: unknown): value is object {
|
|
86
|
+
return value !== null && typeof value === 'object';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatLeaf(value: unknown): string {
|
|
90
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
91
|
+
if (typeof value === 'number') {
|
|
92
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
93
|
+
}
|
|
94
|
+
return String(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One tree node. Expansion is per-node LOCAL state (stable across polls —
|
|
99
|
+
* React keeps state for a keyed position), and children of a collapsed node
|
|
100
|
+
* are NOT mounted: a fresh snapshot every poll re-renders only the visible
|
|
101
|
+
* rows, never a collapsed subtree.
|
|
102
|
+
*/
|
|
103
|
+
function StateTreeNode({ name, value, depth }: { name: string; value: unknown; depth: number }) {
|
|
104
|
+
const [expanded, setExpanded] = useState(depth === 0);
|
|
105
|
+
const rowStyle: React.CSSProperties = {
|
|
106
|
+
...MONO,
|
|
107
|
+
paddingLeft: 8 + depth * 14,
|
|
108
|
+
lineHeight: '18px',
|
|
109
|
+
whiteSpace: 'nowrap',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (!isBranch(value)) {
|
|
113
|
+
return (
|
|
114
|
+
<div style={rowStyle} data-testid="net-tree-leaf">
|
|
115
|
+
<span style={{ color: themeVars.content.primary }}>{name}</span>
|
|
116
|
+
<span style={{ color: themeVars.content.muted }}>: </span>
|
|
117
|
+
<span style={{ color: themeVars.content.primary }}>{formatLeaf(value)}</span>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<div
|
|
126
|
+
style={{ ...rowStyle, cursor: 'pointer', userSelect: 'none' }}
|
|
127
|
+
data-testid="net-tree-branch"
|
|
128
|
+
data-expanded={expanded || undefined}
|
|
129
|
+
onClick={() => setExpanded((e) => !e)}
|
|
130
|
+
>
|
|
131
|
+
<span style={{ color: themeVars.content.muted, display: 'inline-block', width: 12 }}>
|
|
132
|
+
<DisclosureIcon direction={expanded ? 'down' : 'right'} />
|
|
133
|
+
</span>
|
|
134
|
+
<span style={{ color: themeVars.content.primary }}>{name}</span>
|
|
135
|
+
<span style={{ color: themeVars.content.dim }}>
|
|
136
|
+
{' '}
|
|
137
|
+
{Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
{expanded &&
|
|
141
|
+
entries.map(([key, child]) => (
|
|
142
|
+
<StateTreeNode key={key} name={key} value={child} depth={depth + 1} />
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Sparklines -----------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
const SPARK_W = 96;
|
|
151
|
+
const SPARK_H = 22;
|
|
152
|
+
|
|
153
|
+
/** Memoized on the DERIVED points string (computed by the caller each
|
|
154
|
+
* render): a sparkline redraws only when a new 1 Hz sample changes the
|
|
155
|
+
* geometry, not on every 250 ms panel poll. (The comparator must not read
|
|
156
|
+
* the mutable RateSeries itself — both props would alias the same mutated
|
|
157
|
+
* instance and compare equal forever, freezing the sparkline.) */
|
|
158
|
+
const Sparkline = memo(function Sparkline({ points, color }: { points: string; color: string }) {
|
|
159
|
+
return (
|
|
160
|
+
<svg
|
|
161
|
+
width={SPARK_W}
|
|
162
|
+
height={SPARK_H}
|
|
163
|
+
style={{
|
|
164
|
+
display: 'block',
|
|
165
|
+
background: themeVars.surface.inset,
|
|
166
|
+
borderRadius: themeVars.shape.small,
|
|
167
|
+
}}
|
|
168
|
+
role="img"
|
|
169
|
+
aria-label="rate sparkline"
|
|
170
|
+
>
|
|
171
|
+
{points && <polyline points={points} fill="none" stroke={color} strokeWidth={1.2} />}
|
|
172
|
+
</svg>
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function RateCell({
|
|
177
|
+
label,
|
|
178
|
+
series,
|
|
179
|
+
unit,
|
|
180
|
+
color,
|
|
181
|
+
testId,
|
|
182
|
+
}: {
|
|
183
|
+
label: string;
|
|
184
|
+
series: RateSeries;
|
|
185
|
+
unit: string;
|
|
186
|
+
color: string;
|
|
187
|
+
testId: string;
|
|
188
|
+
}) {
|
|
189
|
+
const latest = series.latest;
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
data-testid={testId}
|
|
193
|
+
style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[1] }}
|
|
194
|
+
>
|
|
195
|
+
<div style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
|
|
196
|
+
{label}{' '}
|
|
197
|
+
<span style={{ color: themeVars.content.primary, fontFamily: themeVars.typography.mono }}>
|
|
198
|
+
{/* An empty series = the adapter omits this direction (e.g. inbound
|
|
199
|
+
bytes aren't observable) — say n/a, never draw a fabricated 0. */}
|
|
200
|
+
{latest === null ? 'n/a' : `${latest}${unit}`}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
<Sparkline points={sparklinePoints(series.data, SPARK_W, SPARK_H)} color={color} />
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Message log ----------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function MessageRow({ event }: { event: NetMessageEvent }) {
|
|
211
|
+
const dirColor = event.direction === 'in' ? themeVars.semantic.success : themeVars.accent.default;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
data-testid="net-log-row"
|
|
215
|
+
data-direction={event.direction}
|
|
216
|
+
style={{
|
|
217
|
+
...MONO,
|
|
218
|
+
display: 'grid',
|
|
219
|
+
gridTemplateColumns: '64px 24px 1fr 56px',
|
|
220
|
+
gap: spaceVar[2],
|
|
221
|
+
lineHeight: '17px',
|
|
222
|
+
padding: `0 ${spaceVar[4]}`,
|
|
223
|
+
opacity: event.dropped ? 0.55 : 1,
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<span style={{ color: themeVars.content.dim }}>{(event.time / 1000).toFixed(2)}s</span>
|
|
227
|
+
<span style={{ color: dirColor }}>{event.direction === 'in' ? '↓in' : '↑out'}</span>
|
|
228
|
+
<span
|
|
229
|
+
style={{
|
|
230
|
+
color: themeVars.content.primary,
|
|
231
|
+
textDecoration: event.dropped ? 'line-through' : 'none',
|
|
232
|
+
overflow: 'hidden',
|
|
233
|
+
textOverflow: 'ellipsis',
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{event.type}
|
|
237
|
+
{event.dropped ? ' (dropped)' : ''}
|
|
238
|
+
</span>
|
|
239
|
+
<span style={{ color: themeVars.content.muted, textAlign: 'right' }}>
|
|
240
|
+
{event.size !== undefined ? `${event.size} B` : '—'}
|
|
241
|
+
</span>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Conditioner ----------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function ConditionerControls({ adapter }: { adapter: NetworkingAdapter }) {
|
|
249
|
+
// Local mirror initialized from the adapter; commits go through
|
|
250
|
+
// setConditioning on gesture end. Re-reading each render would fight the
|
|
251
|
+
// NumberInput drag, so the adapter's value seeds state once per mount.
|
|
252
|
+
const [cond, setCond] = useState<NetConditioning>(
|
|
253
|
+
() => adapter.getConditioning?.() ?? { latencyMs: 0, jitterMs: 0, packetLoss: 0 },
|
|
254
|
+
);
|
|
255
|
+
const apply = (next: NetConditioning) => {
|
|
256
|
+
setCond(next);
|
|
257
|
+
adapter.setConditioning?.(next);
|
|
258
|
+
};
|
|
259
|
+
return (
|
|
260
|
+
<div
|
|
261
|
+
data-testid="net-conditioner"
|
|
262
|
+
style={{
|
|
263
|
+
display: 'flex',
|
|
264
|
+
gap: spaceVar[4],
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>Conditioner</span>
|
|
270
|
+
<NumberInput
|
|
271
|
+
label="latency ms"
|
|
272
|
+
value={cond.latencyMs}
|
|
273
|
+
step={10}
|
|
274
|
+
testId="net-cond-latency"
|
|
275
|
+
onChange={(v) => apply({ ...cond, latencyMs: Math.max(0, v) })}
|
|
276
|
+
/>
|
|
277
|
+
<NumberInput
|
|
278
|
+
label="jitter ms"
|
|
279
|
+
value={cond.jitterMs}
|
|
280
|
+
step={5}
|
|
281
|
+
testId="net-cond-jitter"
|
|
282
|
+
onChange={(v) => apply({ ...cond, jitterMs: Math.max(0, v) })}
|
|
283
|
+
/>
|
|
284
|
+
<NumberInput
|
|
285
|
+
label="loss %"
|
|
286
|
+
value={cond.packetLoss * 100}
|
|
287
|
+
step={5}
|
|
288
|
+
testId="net-cond-loss"
|
|
289
|
+
onChange={(v) => apply({ ...cond, packetLoss: Math.min(1, Math.max(0, v / 100)) })}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Player identity ------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
/** The local player's name: an EDITABLE field when the adapter can set it
|
|
298
|
+
* (`setPlayerIdentity`), a read-only label when it can only read it. Present
|
|
299
|
+
* in the header only when the game wired identity at all — never fabricated. */
|
|
300
|
+
function IdentityControls({
|
|
301
|
+
adapter,
|
|
302
|
+
settable,
|
|
303
|
+
}: {
|
|
304
|
+
adapter: NetworkingAdapter;
|
|
305
|
+
settable: boolean;
|
|
306
|
+
}) {
|
|
307
|
+
// Seed once per mount from the adapter (re-reading each render would fight
|
|
308
|
+
// typing), same as the conditioner. The live value is re-seeded when the
|
|
309
|
+
// adapter identity changes via `key` on the parent.
|
|
310
|
+
const [name, setName] = useState(() => adapter.getPlayerIdentity?.()?.name ?? '');
|
|
311
|
+
const commit = () => {
|
|
312
|
+
const next = name.trim();
|
|
313
|
+
if (next) void adapter.setPlayerIdentity?.({ name: next });
|
|
314
|
+
};
|
|
315
|
+
return (
|
|
316
|
+
<div
|
|
317
|
+
data-testid="net-identity"
|
|
318
|
+
style={{
|
|
319
|
+
display: 'flex',
|
|
320
|
+
gap: spaceVar[4],
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>Player name</span>
|
|
326
|
+
{settable ? (
|
|
327
|
+
<TextInput
|
|
328
|
+
data-testid="net-identity-name"
|
|
329
|
+
value={name}
|
|
330
|
+
onChange={(e) => setName(e.target.value)}
|
|
331
|
+
onBlur={commit}
|
|
332
|
+
onKeyDown={(e) => {
|
|
333
|
+
if (e.key === 'Enter') e.currentTarget.blur();
|
|
334
|
+
}}
|
|
335
|
+
style={{ width: 140 }}
|
|
336
|
+
/>
|
|
337
|
+
) : (
|
|
338
|
+
<span data-testid="net-identity-name" style={{ ...MONO, color: themeVars.content.primary }}>
|
|
339
|
+
{name || '—'}
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Header ---------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/** Connection / room / replication-stat / rate row. Extracted from the panel
|
|
349
|
+
* so each half stays readable: every cell here is a present-or-absent
|
|
350
|
+
* branch, and the panel below owns state and the body layout. */
|
|
351
|
+
function NetworkHeader({
|
|
352
|
+
view,
|
|
353
|
+
caps,
|
|
354
|
+
rates,
|
|
355
|
+
}: {
|
|
356
|
+
view: NetworkView;
|
|
357
|
+
caps: NetworkCapabilityMap;
|
|
358
|
+
rates: RateHistory;
|
|
359
|
+
}) {
|
|
360
|
+
return (
|
|
361
|
+
<div
|
|
362
|
+
style={{
|
|
363
|
+
display: 'flex',
|
|
364
|
+
flexWrap: 'wrap',
|
|
365
|
+
gap: spaceVar[6],
|
|
366
|
+
alignItems: 'center',
|
|
367
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
368
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
{view.connectionState !== undefined ? (
|
|
372
|
+
<span
|
|
373
|
+
data-testid="net-connection-state"
|
|
374
|
+
style={{ color: STATE_COLORS[view.connectionState] }}
|
|
375
|
+
>
|
|
376
|
+
● {view.connectionState}
|
|
377
|
+
</span>
|
|
378
|
+
) : (
|
|
379
|
+
<AbsentNote testId="net-connection-state-absent">
|
|
380
|
+
Connection state not provided by this adapter.
|
|
381
|
+
</AbsentNote>
|
|
382
|
+
)}
|
|
383
|
+
{view.roomInfo === undefined ? (
|
|
384
|
+
<AbsentNote testId="net-room-absent">Room info not provided by this adapter.</AbsentNote>
|
|
385
|
+
) : view.roomInfo ? (
|
|
386
|
+
<span style={{ ...MONO, color: themeVars.content.primary }}>
|
|
387
|
+
{view.roomInfo.roomName} · {view.roomInfo.roomId} · session {view.roomInfo.sessionId}
|
|
388
|
+
</span>
|
|
389
|
+
) : (
|
|
390
|
+
<span style={{ color: themeVars.content.muted }}>no room</span>
|
|
391
|
+
)}
|
|
392
|
+
{view.stats ? (
|
|
393
|
+
<span style={{ color: themeVars.content.muted }}>
|
|
394
|
+
entities <span style={{ color: themeVars.content.primary }}>{view.stats.entities}</span>
|
|
395
|
+
</span>
|
|
396
|
+
) : (
|
|
397
|
+
<AbsentNote testId="net-stats-absent">
|
|
398
|
+
Replication stats not provided by this adapter.
|
|
399
|
+
</AbsentNote>
|
|
400
|
+
)}
|
|
401
|
+
{caps.rates ? (
|
|
402
|
+
<div data-testid="net-sparklines" style={{ display: 'flex', gap: spaceVar[5] }}>
|
|
403
|
+
<RateCell
|
|
404
|
+
label="msgs in"
|
|
405
|
+
unit="/s"
|
|
406
|
+
series={rates.msgsIn}
|
|
407
|
+
color={themeVars.semantic.success}
|
|
408
|
+
testId="net-rate-msgs-in"
|
|
409
|
+
/>
|
|
410
|
+
<RateCell
|
|
411
|
+
label="msgs out"
|
|
412
|
+
unit="/s"
|
|
413
|
+
series={rates.msgsOut}
|
|
414
|
+
color={themeVars.accent.default}
|
|
415
|
+
testId="net-rate-msgs-out"
|
|
416
|
+
/>
|
|
417
|
+
<RateCell
|
|
418
|
+
label="bytes in"
|
|
419
|
+
unit=" B/s"
|
|
420
|
+
series={rates.bytesIn}
|
|
421
|
+
color={themeVars.semantic.success}
|
|
422
|
+
testId="net-rate-bytes-in"
|
|
423
|
+
/>
|
|
424
|
+
<RateCell
|
|
425
|
+
label="bytes out"
|
|
426
|
+
unit=" B/s"
|
|
427
|
+
series={rates.bytesOut}
|
|
428
|
+
color={themeVars.accent.default}
|
|
429
|
+
testId="net-rate-bytes-out"
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
) : (
|
|
433
|
+
<AbsentNote testId="net-rates-absent">Rates not provided by this adapter.</AbsentNote>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- The panel ------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
export function NetworkInspectorPanel() {
|
|
442
|
+
const [, force] = useReducer((c: number) => c + 1, 0);
|
|
443
|
+
const logRef = useRef(new MessageLogModel());
|
|
444
|
+
const ratesRef = useRef(new RateHistory());
|
|
445
|
+
const adapterRef = useRef<NetworkingAdapter | null>(null);
|
|
446
|
+
const [paused, setPaused] = useState(false);
|
|
447
|
+
const [filter, setFilter] = useState('');
|
|
448
|
+
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
const syncAdapter = () => {
|
|
451
|
+
const adapter = editorHost().systems.inspectedNetworking() ?? null;
|
|
452
|
+
if (adapter !== adapterRef.current) {
|
|
453
|
+
// New mount (or unmount): the old game's log/rate history is stale.
|
|
454
|
+
adapterRef.current = adapter;
|
|
455
|
+
logRef.current = new MessageLogModel();
|
|
456
|
+
logRef.current.paused = paused;
|
|
457
|
+
ratesRef.current = new RateHistory();
|
|
458
|
+
}
|
|
459
|
+
return adapter;
|
|
460
|
+
};
|
|
461
|
+
// 250 ms consume/render poll — also covers the
|
|
462
|
+
// adapter's IDENTITY changing when a game mounts/unmounts)…
|
|
463
|
+
const tick = () => {
|
|
464
|
+
const adapter = syncAdapter();
|
|
465
|
+
logRef.current.pull(adapter);
|
|
466
|
+
force();
|
|
467
|
+
};
|
|
468
|
+
tick();
|
|
469
|
+
const pollId = setInterval(tick, 250);
|
|
470
|
+
// …and a 1 Hz sparkline sampler (rates are whole-second windows).
|
|
471
|
+
const rateId = setInterval(() => {
|
|
472
|
+
const adapter = syncAdapter();
|
|
473
|
+
const rates = adapter?.getRates?.();
|
|
474
|
+
if (rates) ratesRef.current.sample(rates);
|
|
475
|
+
}, 1000);
|
|
476
|
+
const unsubscribe = editorHost()
|
|
477
|
+
.systems.inspectedNetworking()
|
|
478
|
+
?.subscribe(() => force());
|
|
479
|
+
return () => {
|
|
480
|
+
clearInterval(pollId);
|
|
481
|
+
clearInterval(rateId);
|
|
482
|
+
unsubscribe?.();
|
|
483
|
+
};
|
|
484
|
+
}, [paused]);
|
|
485
|
+
|
|
486
|
+
const adapter = adapterRef.current ?? editorHost().systems.inspectedNetworking() ?? null;
|
|
487
|
+
const caps = deriveNetworkCapabilities(adapter);
|
|
488
|
+
|
|
489
|
+
if (!adapter || !caps) {
|
|
490
|
+
return (
|
|
491
|
+
<div
|
|
492
|
+
data-testid="network-inspector-empty"
|
|
493
|
+
style={{
|
|
494
|
+
padding: spaceVar[8],
|
|
495
|
+
color: themeVars.content.muted,
|
|
496
|
+
fontSize: fontSizeVar.md,
|
|
497
|
+
lineHeight: '18px',
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
<div style={{ color: themeVars.content.primary, marginBottom: spaceVar[2] }}>
|
|
501
|
+
No networking adapter is registered.
|
|
502
|
+
</div>
|
|
503
|
+
Games expose replication state to this inspector by registering a{' '}
|
|
504
|
+
<code style={MONO}>NetworkingAdapter</code> during setup:{' '}
|
|
505
|
+
<code style={MONO}>{"ctx.registerSystemAdapter?.('networking', adapter)"}</code> — see the
|
|
506
|
+
third-person-arena example. Single-player games register none; there is nothing to inspect.
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// `undefined` here means NO READER, which the header renders as an absent
|
|
512
|
+
// note — never as `disconnected` / `no room` / `entities 0`, each of which is
|
|
513
|
+
// a measurement claim the adapter never made. See `deriveNetworkView`.
|
|
514
|
+
const view = deriveNetworkView(adapter);
|
|
515
|
+
const snapshot = caps.stateTree ? adapter.getStateSnapshot?.() : undefined;
|
|
516
|
+
const messages = filterMessages(logRef.current.all, filter);
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<div
|
|
520
|
+
data-testid="network-inspector"
|
|
521
|
+
// §2.31 P2 amendment: text-dense output region reads over a local
|
|
522
|
+
// frosted layer (inert for non-frost themes).
|
|
523
|
+
className="vgai-content-frost"
|
|
524
|
+
style={{
|
|
525
|
+
flex: 1,
|
|
526
|
+
minHeight: 0,
|
|
527
|
+
display: 'flex',
|
|
528
|
+
flexDirection: 'column',
|
|
529
|
+
fontSize: fontSizeVar.md,
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
{/* Header: connection / room / rates / conditioner */}
|
|
533
|
+
<NetworkHeader view={view} caps={caps} rates={ratesRef.current} />
|
|
534
|
+
|
|
535
|
+
{caps.identity ? (
|
|
536
|
+
// `key` re-seeds the field when the game reports a new name (e.g. the
|
|
537
|
+
// server assigned one on join) so the input isn't pinned to a stale mount value.
|
|
538
|
+
<IdentityControls
|
|
539
|
+
key={adapter.getPlayerIdentity?.()?.name ?? ''}
|
|
540
|
+
adapter={adapter}
|
|
541
|
+
settable={caps.identitySettable}
|
|
542
|
+
/>
|
|
543
|
+
) : null}
|
|
544
|
+
|
|
545
|
+
{caps.conditioning ? (
|
|
546
|
+
<ConditionerControls adapter={adapter} />
|
|
547
|
+
) : (
|
|
548
|
+
<AbsentNote testId="net-conditioner-absent">
|
|
549
|
+
Link conditioning not provided by this adapter.
|
|
550
|
+
</AbsentNote>
|
|
551
|
+
)}
|
|
552
|
+
|
|
553
|
+
{/* Body: state tree | message log */}
|
|
554
|
+
<div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
|
|
555
|
+
<div
|
|
556
|
+
data-testid="net-state-tree"
|
|
557
|
+
style={{
|
|
558
|
+
flex: 1,
|
|
559
|
+
minWidth: 0,
|
|
560
|
+
overflow: 'auto',
|
|
561
|
+
borderRight: `1px solid ${themeVars.boundary.default}`,
|
|
562
|
+
padding: `${spaceVar[2]} 0`,
|
|
563
|
+
}}
|
|
564
|
+
>
|
|
565
|
+
{caps.stateTree ? (
|
|
566
|
+
snapshot != null ? (
|
|
567
|
+
<StateTreeNode name="state" value={snapshot} depth={0} />
|
|
568
|
+
) : (
|
|
569
|
+
<AbsentNote>Not connected — no replicated state to show.</AbsentNote>
|
|
570
|
+
)
|
|
571
|
+
) : (
|
|
572
|
+
<AbsentNote testId="net-tree-absent">
|
|
573
|
+
Replicated-state snapshot not provided by this adapter.
|
|
574
|
+
</AbsentNote>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div
|
|
579
|
+
data-testid="net-message-log"
|
|
580
|
+
style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}
|
|
581
|
+
>
|
|
582
|
+
{caps.messageLog ? (
|
|
583
|
+
<>
|
|
584
|
+
<div
|
|
585
|
+
style={{
|
|
586
|
+
display: 'flex',
|
|
587
|
+
gap: spaceVar[3],
|
|
588
|
+
alignItems: 'center',
|
|
589
|
+
padding: spaceVar[2],
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
<Button
|
|
593
|
+
type="button"
|
|
594
|
+
variant="ghost"
|
|
595
|
+
data-testid="net-log-pause"
|
|
596
|
+
onClick={() => {
|
|
597
|
+
const next = !paused;
|
|
598
|
+
logRef.current.paused = next;
|
|
599
|
+
setPaused(next);
|
|
600
|
+
}}
|
|
601
|
+
>
|
|
602
|
+
{paused ? 'Resume' : 'Pause'}
|
|
603
|
+
</Button>
|
|
604
|
+
<Button
|
|
605
|
+
type="button"
|
|
606
|
+
variant="ghost"
|
|
607
|
+
data-testid="net-log-clear"
|
|
608
|
+
onClick={() => {
|
|
609
|
+
logRef.current.clear();
|
|
610
|
+
force();
|
|
611
|
+
}}
|
|
612
|
+
>
|
|
613
|
+
Clear
|
|
614
|
+
</Button>
|
|
615
|
+
<TextInput
|
|
616
|
+
data-testid="net-log-filter"
|
|
617
|
+
placeholder="Filter by type"
|
|
618
|
+
value={filter}
|
|
619
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
620
|
+
style={{ flex: 1, minWidth: 60 }}
|
|
621
|
+
/>
|
|
622
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
|
|
623
|
+
{messages.length}/{logRef.current.all.length}
|
|
624
|
+
</span>
|
|
625
|
+
</div>
|
|
626
|
+
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
|
627
|
+
{messages.length === 0 ? (
|
|
628
|
+
<AbsentNote>No messages captured yet.</AbsentNote>
|
|
629
|
+
) : (
|
|
630
|
+
// Newest last, natural reading order for a tail-follow log.
|
|
631
|
+
messages.map((event) => <MessageRow key={event.seq} event={event} />)
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
634
|
+
</>
|
|
635
|
+
) : (
|
|
636
|
+
<AbsentNote testId="net-log-absent">
|
|
637
|
+
Message log not provided by this adapter.
|
|
638
|
+
</AbsentNote>
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
);
|
|
644
|
+
}
|