@noya-app/noya-multiplayer-react 0.1.62 → 0.1.64

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.
@@ -1,246 +1,39 @@
1
+ /* eslint-disable no-restricted-globals */
1
2
  "use client";
2
3
 
3
4
  import { Input, OutputTransform } from "@noya-app/noya-schemas";
4
- import { memoGeneric } from "@noya-app/react-utils";
5
- import { ExtendedPathKey, NoyaManager } from "@noya-app/state-manager";
5
+ import { Base64, uuid } from "@noya-app/noya-utils";
6
+ import { downloadBlob, memoGeneric } from "@noya-app/react-utils";
7
+ import { NoyaManager } from "@noya-app/state-manager";
6
8
  import React, {
7
9
  CSSProperties,
8
10
  ComponentPropsWithoutRef,
9
- FC,
11
+ useCallback,
10
12
  useEffect,
11
13
  useLayoutEffect,
12
14
  } from "react";
13
- import {
14
- ObjectInspector,
15
- ObjectLabel,
16
- ObjectName,
17
- ObjectPreview,
18
- chromeDark,
19
- chromeLight,
20
- } from "react-inspector";
15
+ import { ObjectInspector } from "react-inspector";
21
16
  import { useManagedHistory } from "../hooks";
22
17
  import { useObservable } from "../useObservable";
18
+ import { ColoredDot } from "./ColoredDot";
19
+ import {
20
+ getStateInspectorBorderColor,
21
+ getStateInspectorTheme,
22
+ } from "./inspectorTheme";
23
+ import { ActivityEventsSection } from "./sections/ActivityEventsSection";
24
+ import { EventsSection } from "./sections/EventsSection";
25
+ import { HistorySection } from "./sections/HistorySection";
26
+ import { exportAll, importAll } from "./serialization";
27
+ import { StateInspectorButton } from "./StateInspectorButton";
28
+ import {
29
+ StateInspectorDisclosureRowInner,
30
+ StateInspectorDisclosureSection,
31
+ } from "./StateInspectorDisclosureSection";
32
+ import { StateInspectorRow } from "./StateInspectorRow";
33
+ import { StateInspectorTitleLabel } from "./StateInspectorTitleLabel";
34
+ import { StateInspectorToggleButton } from "./StateInspectorToggleButton";
23
35
  import { useLocalStorageState } from "./useLocalStorageState";
24
-
25
- const lightTheme = {
26
- ...chromeLight,
27
- BASE_BACKGROUND_COLOR: "transparent",
28
- OBJECT_NAME_COLOR: "rgba(0,0,0,0.7)",
29
- } as any;
30
-
31
- const darkTheme = {
32
- ...chromeDark,
33
- BASE_BACKGROUND_COLOR: "transparent",
34
- OBJECT_NAME_COLOR: "rgba(255,255,255,0.7)",
35
- } as any;
36
-
37
- const styles = {
38
- sectionInner: {
39
- flex: "1 1 0",
40
- overflowY: "auto",
41
- overflowX: "hidden",
42
- display: "flex",
43
- flexDirection: "column",
44
- },
45
- } as const;
46
-
47
- function ToggleButton({
48
- showInspector,
49
- setShowInspector,
50
- theme,
51
- anchor,
52
- }: {
53
- showInspector: boolean;
54
- setShowInspector: (value: boolean) => void;
55
- theme: typeof chromeLight;
56
- anchor: StateInspectorAnchor;
57
- }) {
58
- const isRightAnchor = anchor === "right";
59
-
60
- const rightIcon = (
61
- <svg
62
- xmlns="http://www.w3.org/2000/svg"
63
- fill="none"
64
- viewBox="0 0 24 24"
65
- strokeWidth={1.5}
66
- stroke="currentColor"
67
- className="size-6"
68
- >
69
- <path
70
- strokeLinecap="round"
71
- strokeLinejoin="round"
72
- d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5"
73
- />
74
- </svg>
75
- );
76
-
77
- const leftIcon = (
78
- <svg
79
- xmlns="http://www.w3.org/2000/svg"
80
- fill="none"
81
- viewBox="0 0 24 24"
82
- strokeWidth={1.5}
83
- stroke="currentColor"
84
- className="size-6"
85
- >
86
- <path
87
- strokeLinecap="round"
88
- strokeLinejoin="round"
89
- d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5"
90
- />
91
- </svg>
92
- );
93
-
94
- return (
95
- <span
96
- role="button"
97
- style={{
98
- flex: "0",
99
- appearance: "none",
100
- color: theme.BASE_COLOR,
101
- border: "none",
102
- fontSize: "12px",
103
- whiteSpace: "nowrap",
104
- display: "inline-flex",
105
- alignItems: "center",
106
- justifyContent: "center",
107
- padding: "2px 0",
108
- }}
109
- onClick={(event) => {
110
- event.stopPropagation();
111
-
112
- setShowInspector(!showInspector);
113
- }}
114
- >
115
- {showInspector ? (
116
- "Hide Inspector"
117
- ) : (
118
- <span style={{ width: "12px", height: "12px" }}>
119
- {isRightAnchor ? rightIcon : leftIcon}
120
- </span>
121
- )}
122
- </span>
123
- );
124
- }
125
-
126
- function DisclosureSection({
127
- open,
128
- setOpen,
129
- title,
130
- right,
131
- children,
132
- colorScheme,
133
- isFirst,
134
- style,
135
- }: {
136
- open: boolean;
137
- setOpen?: (value: boolean) => void;
138
- title: React.ReactNode;
139
- right?: React.ReactNode;
140
- children: React.ReactNode;
141
- colorScheme: "light" | "dark";
142
- isFirst?: boolean;
143
- style?: CSSProperties;
144
- }) {
145
- const theme = colorScheme === "light" ? lightTheme : darkTheme;
146
-
147
- const borderColor =
148
- colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
149
-
150
- return (
151
- <div
152
- style={{
153
- flex: open ? "1 1 0" : "0",
154
- display: "flex",
155
- flexDirection: "column",
156
- ...style,
157
- }}
158
- >
159
- <div
160
- onClick={() => setOpen?.(!open)}
161
- style={{
162
- cursor: "default",
163
- fontSize: "12px",
164
- padding: "4px 10px",
165
- display: "flex",
166
- alignItems: "center",
167
- ...(!isFirst && { borderTop: `1px solid ${borderColor}` }),
168
- ...(open && { borderBottom: `1px solid ${borderColor}` }),
169
- }}
170
- >
171
- {setOpen && (
172
- <Arrow
173
- expanded={open}
174
- style={{
175
- fontSize: theme.ARROW_FONT_SIZE,
176
- marginRight: theme.ARROW_MARGIN_RIGHT + 1,
177
- color: theme.ARROW_COLOR,
178
- }}
179
- />
180
- )}
181
- <span style={{ flex: "1 1 0", userSelect: "none" }}>{title}</span>
182
- {right}
183
- </div>
184
- {open && children}
185
- </div>
186
- );
187
- }
188
-
189
- function InspectorRow({
190
- children,
191
- colorScheme,
192
- selected,
193
- style,
194
- variant,
195
- }: {
196
- children: React.ReactNode;
197
- colorScheme: "light" | "dark";
198
- selected?: boolean;
199
- style?: CSSProperties;
200
- variant?: "up" | "down";
201
- }) {
202
- const solidBorderColor =
203
- colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
204
-
205
- return (
206
- <div
207
- style={{
208
- borderBottom: `1px solid ${solidBorderColor}`,
209
- fontSize: "12px",
210
- fontFamily: "Menlo, monospace",
211
- padding: "2px 12px 1px",
212
- display: "flex",
213
- alignItems: "center",
214
- background:
215
- variant === "up"
216
- ? "rgba(0,255,0,0.1)"
217
- : variant === "down"
218
- ? "transparent"
219
- : selected
220
- ? "rgb(59 130 246 / 15%)"
221
- : undefined,
222
- // background:
223
- // colorScheme === "light"
224
- // ? "rgba(0,0,0,0.05)"
225
- // : "rgba(255,255,255,0.05)",
226
- ...style,
227
- }}
228
- >
229
- <span
230
- style={{
231
- fontFamily: "Menlo, monospace",
232
- fontSize: "11px",
233
- borderRadius: 4,
234
- display: "inline-block",
235
- }}
236
- >
237
- {children}
238
- </span>
239
- </div>
240
- );
241
- }
242
-
243
- const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
36
+ import { ellipsis, uploadFile } from "./utils";
244
37
 
245
38
  export type StateInspectorAnchor =
246
39
  | "left"
@@ -278,6 +71,8 @@ export const StateInspector = memoGeneric(function StateInspector<
278
71
  connectionEventManager,
279
72
  taskManager,
280
73
  ioManager,
74
+ resourceManager,
75
+ activityEventsManager,
281
76
  } = noyaManager;
282
77
 
283
78
  const [didMount, setDidMount] = React.useState(false);
@@ -290,7 +85,7 @@ export const StateInspector = memoGeneric(function StateInspector<
290
85
  }, []);
291
86
 
292
87
  const eventsContainerRef = React.useRef<HTMLDivElement>(null);
293
- const historyContainerRef = React.useRef<HTMLDivElement>(null);
88
+ const activityEventsContainerRef = React.useRef<HTMLDivElement>(null);
294
89
 
295
90
  const [showInspector, setShowInspector] = useLocalStorageState(
296
91
  "noya-multiplayer-react-show-inspector",
@@ -324,6 +119,10 @@ export const StateInspector = memoGeneric(function StateInspector<
324
119
  "noya-multiplayer-react-show-assets",
325
120
  false
326
121
  );
122
+ const [showResources, setShowResources] = useLocalStorageState(
123
+ "noya-multiplayer-react-show-resources",
124
+ false
125
+ );
327
126
  const [showSecrets, setShowSecrets] = useLocalStorageState(
328
127
  "noya-multiplayer-react-show-secrets",
329
128
  false
@@ -336,9 +135,12 @@ export const StateInspector = memoGeneric(function StateInspector<
336
135
  "noya-multiplayer-react-show-output-transforms",
337
136
  false
338
137
  );
138
+ const [showActivityEvents, setShowActivityEvents] = useLocalStorageState(
139
+ "noya-multiplayer-react-show-activity-events",
140
+ false
141
+ );
339
142
 
340
143
  const connectionEvents = useObservable(connectionEventManager.events$);
341
-
342
144
  useEffect(() => {
343
145
  if (eventsContainerRef.current) {
344
146
  eventsContainerRef.current.scrollTop =
@@ -352,6 +154,8 @@ export const StateInspector = memoGeneric(function StateInspector<
352
154
  const ephemeral = useObservable(ephemeralUserDataManager.data$);
353
155
  const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
354
156
  const assets = useObservable(assetManager.assets$);
157
+ const resources = useObservable(resourceManager.resources$);
158
+ const resourcesInitialized = useObservable(resourceManager.isInitialized$);
355
159
  const assetsInitialized = useObservable(assetManager.isInitialized$);
356
160
  const secrets = useObservable(secretManager.secrets$);
357
161
  const secretsInitialized = useObservable(secretManager.isInitialized$);
@@ -362,35 +166,8 @@ export const StateInspector = memoGeneric(function StateInspector<
362
166
  const outputTransformsInitialized = useObservable(
363
167
  ioManager.outputTransformsInitialized$
364
168
  );
365
-
366
- useEffect(() => {
367
- if (historyContainerRef.current) {
368
- historyContainerRef.current.scrollTop =
369
- historyContainerRef.current.scrollHeight;
370
- }
371
- }, [historySnapshot]);
372
-
373
- // If history index changes, scroll to the new history entry into view
374
- useEffect(() => {
375
- if (!historyContainerRef.current) return;
376
-
377
- const historyEntry = historyContainerRef.current.querySelector(
378
- `#${HISTORY_ELEMENT_PREFIX}${historySnapshot.historyIndex}`
379
- );
380
-
381
- if (historyEntry) {
382
- historyEntry.scrollIntoView({
383
- block: "nearest",
384
- inline: "nearest",
385
- });
386
- }
387
- }, [historySnapshot.historyIndex]);
388
-
389
- const theme = colorScheme === "light" ? lightTheme : darkTheme;
390
- // const borderColor =
391
- // colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
392
- const solidBorderColor =
393
- colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
169
+ const theme = getStateInspectorTheme(colorScheme);
170
+ const solidBorderColor = getStateInspectorBorderColor(colorScheme);
394
171
 
395
172
  const baseStyle: CSSProperties = {
396
173
  position: "fixed",
@@ -413,6 +190,25 @@ export const StateInspector = memoGeneric(function StateInspector<
413
190
  lineHeight: "13px",
414
191
  };
415
192
 
193
+ const handleExportAll = useCallback(async () => {
194
+ const buffer = await exportAll(noyaManager);
195
+
196
+ downloadBlob(new Blob([buffer]), "state.zip");
197
+ }, [noyaManager]);
198
+
199
+ const handleImportAll = useCallback(async () => {
200
+ const file = await uploadFile();
201
+ const buffer = await file.arrayBuffer();
202
+
203
+ importAll(buffer, noyaManager, {
204
+ shouldAllowSchemaChange: () => {
205
+ return confirm(
206
+ `The imported state uses a different schema than the schema this app expects. Do you want to continue?`
207
+ );
208
+ },
209
+ });
210
+ }, [noyaManager]);
211
+
416
212
  if (!didMount) return null;
417
213
 
418
214
  if (!showInspector) {
@@ -429,7 +225,7 @@ export const StateInspector = memoGeneric(function StateInspector<
429
225
  }}
430
226
  onClick={() => setShowInspector(true)}
431
227
  >
432
- <ToggleButton
228
+ <StateInspectorToggleButton
433
229
  showInspector={showInspector}
434
230
  setShowInspector={setShowInspector}
435
231
  theme={theme}
@@ -447,7 +243,7 @@ export const StateInspector = memoGeneric(function StateInspector<
447
243
  ...props.style,
448
244
  }}
449
245
  >
450
- <DisclosureSection
246
+ <StateInspectorDisclosureSection
451
247
  isFirst
452
248
  open={showUsers}
453
249
  setOpen={setShowUsers}
@@ -458,7 +254,7 @@ export const StateInspector = memoGeneric(function StateInspector<
458
254
  maxHeight: "200px",
459
255
  }}
460
256
  right={
461
- <ToggleButton
257
+ <StateInspectorToggleButton
462
258
  showInspector={showInspector}
463
259
  setShowInspector={setShowInspector}
464
260
  theme={theme}
@@ -466,9 +262,9 @@ export const StateInspector = memoGeneric(function StateInspector<
466
262
  />
467
263
  }
468
264
  >
469
- <div style={{ ...styles.sectionInner, flex: "0 0 auto" }}>
265
+ <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
470
266
  {connectedUsers?.map((user) => (
471
- <InspectorRow
267
+ <StateInspectorRow
472
268
  key={user.id}
473
269
  colorScheme={colorScheme}
474
270
  variant={user.id === userId ? "up" : undefined}
@@ -490,7 +286,7 @@ export const StateInspector = memoGeneric(function StateInspector<
490
286
  />
491
287
  )}
492
288
  {user.name} ({ellipsis(user.id.toString(), 8, "middle")})
493
- </InspectorRow>
289
+ </StateInspectorRow>
494
290
  ))}
495
291
  {!connectedUsers && (
496
292
  <div
@@ -505,16 +301,16 @@ export const StateInspector = memoGeneric(function StateInspector<
505
301
  <span>No connected users</span>
506
302
  </div>
507
303
  )}
508
- </div>
509
- </DisclosureSection>
510
- <DisclosureSection
304
+ </StateInspectorDisclosureRowInner>
305
+ </StateInspectorDisclosureSection>
306
+ <StateInspectorDisclosureSection
511
307
  title={
512
- <TitleLabel>
308
+ <StateInspectorTitleLabel>
513
309
  Data
514
310
  <ColoredDot
515
311
  type={multipeerStateInitialized ? "success" : "error"}
516
312
  />
517
- </TitleLabel>
313
+ </StateInspectorTitleLabel>
518
314
  }
519
315
  colorScheme={colorScheme}
520
316
  setOpen={setShowData}
@@ -523,10 +319,16 @@ export const StateInspector = memoGeneric(function StateInspector<
523
319
  <span
524
320
  style={{
525
321
  display: "flex",
526
- gap: "4px",
322
+ gap: "12px",
527
323
  }}
528
324
  >
529
- <SmallButton
325
+ <StateInspectorButton theme={theme} onClick={handleImportAll}>
326
+ Import
327
+ </StateInspectorButton>
328
+ <StateInspectorButton theme={theme} onClick={handleExportAll}>
329
+ Export
330
+ </StateInspectorButton>
331
+ <StateInspectorButton
530
332
  // disabled={!historySnapshot.canUndo}
531
333
  theme={theme}
532
334
  onClick={() => {
@@ -534,207 +336,63 @@ export const StateInspector = memoGeneric(function StateInspector<
534
336
  }}
535
337
  >
536
338
  Reset
537
- </SmallButton>
339
+ </StateInspectorButton>
538
340
  </span>
539
341
  }
540
342
  >
541
- <div style={styles.sectionInner}>
542
- <InspectorRow colorScheme={colorScheme}>
343
+ <StateInspectorDisclosureRowInner>
344
+ <StateInspectorRow colorScheme={colorScheme}>
543
345
  <ObjectInspector
544
346
  name={multiplayerStateManager.schema ? "state" : undefined}
545
347
  data={state}
546
348
  theme={theme}
547
349
  />
548
- </InspectorRow>
350
+ </StateInspectorRow>
549
351
  {multiplayerStateManager.schema && (
550
- <InspectorRow colorScheme={colorScheme}>
352
+ <StateInspectorRow colorScheme={colorScheme}>
551
353
  <ObjectInspector
552
354
  name="schema"
553
355
  data={multiplayerStateManager.schema}
554
356
  theme={theme}
555
357
  />
556
- </InspectorRow>
358
+ </StateInspectorRow>
557
359
  )}
558
- </div>
559
- </DisclosureSection>
560
- <DisclosureSection
561
- open={showHistory}
562
- setOpen={setShowHistory}
563
- title="History"
360
+ </StateInspectorDisclosureRowInner>
361
+ </StateInspectorDisclosureSection>
362
+ <HistorySection
363
+ showHistory={showHistory}
364
+ setShowHistory={setShowHistory}
564
365
  colorScheme={colorScheme}
565
- right={
566
- <span
567
- style={{
568
- display: "flex",
569
- gap: "4px",
570
- }}
571
- >
572
- <SmallButton
573
- disabled={!historySnapshot.canUndo}
574
- theme={theme}
575
- onClick={() => {
576
- multiplayerStateManager.undo();
577
- }}
578
- >
579
- <span style={{ width: "12px", height: "12px" }}>
580
- <svg
581
- xmlns="http://www.w3.org/2000/svg"
582
- width="1em"
583
- height="1em"
584
- viewBox="0 0 20 20"
585
- >
586
- <path
587
- fill="currentColor"
588
- d="M12 5H7V2L1 6l6 4V7h5c2.2 0 4 1.8 4 4s-1.8 4-4 4H7v2h5c3.3 0 6-2.7 6-6s-2.7-6-6-6"
589
- />
590
- </svg>
591
- </span>
592
- </SmallButton>
593
- <SmallButton
594
- disabled={!historySnapshot.canRedo}
595
- theme={theme}
596
- onClick={() => {
597
- multiplayerStateManager.redo();
598
- }}
599
- >
600
- <span style={{ width: "12px", height: "12px" }}>
601
- <svg
602
- xmlns="http://www.w3.org/2000/svg"
603
- width="1em"
604
- height="1em"
605
- viewBox="0 0 20 20"
606
- >
607
- <path
608
- fill="currentColor"
609
- d="M8 5h5V2l6 4l-6 4V7H8c-2.2 0-4 1.8-4 4s1.8 4 4 4h5v2H8c-3.3 0-6-2.7-6-6s2.7-6 6-6"
610
- />
611
- </svg>
612
- </span>
613
- </SmallButton>
614
- </span>
615
- }
616
- >
617
- <div ref={historyContainerRef} style={styles.sectionInner}>
618
- <div
619
- id={`${HISTORY_ELEMENT_PREFIX}0`}
620
- style={{
621
- borderBottom: `1px solid ${solidBorderColor}`,
622
- fontSize: "12px",
623
- fontFamily: "Menlo, monospace",
624
- padding: "2px 12px 1px",
625
- display: "flex",
626
- alignItems: "center",
627
- background:
628
- historySnapshot.historyIndex === 0
629
- ? "rgb(59 130 246 / 15%)"
630
- : undefined,
631
- }}
632
- >
633
- <span
634
- style={{
635
- fontFamily: "Menlo, monospace",
636
- fontSize: "11px",
637
- borderRadius: 4,
638
- display: "inline-block",
639
- }}
640
- >
641
- Initial state
642
- </span>
643
- </div>
644
- {historySnapshot.history.map((entry, index) => (
645
- <div
646
- id={`${HISTORY_ELEMENT_PREFIX}${index + 1}`}
647
- key={index}
648
- style={{
649
- padding: "0px 12px 1px",
650
- borderBottom: `1px solid ${solidBorderColor}`,
651
- background:
652
- index + 1 === historySnapshot.historyIndex
653
- ? "rgb(59 130 246 / 15%)"
654
- : undefined,
655
- }}
656
- >
657
- {"name" in entry.metadata &&
658
- typeof entry.metadata.name === "string" && (
659
- <pre
660
- style={{
661
- fontSize: "11px",
662
- display: "flex",
663
- flexWrap: "wrap",
664
- margin: 0,
665
- }}
666
- >
667
- {entry.metadata.name}
668
- </pre>
669
- )}
670
- {entry.redoPatches?.map((patch, j) => (
671
- <pre
672
- key={j}
673
- style={{
674
- fontSize: "11px",
675
- display: "flex",
676
- flexWrap: "wrap",
677
- margin: 0,
678
- }}
679
- >
680
- {patch.op === "add" && (
681
- <>
682
- <span style={{ fontStyle: "italic" }}>
683
- {pathToString(patch.path)}
684
- </span>
685
- {" = "}
686
- <ObjectInspector data={patch.value} theme={theme} />
687
- </>
688
- )}
689
- {patch.op === "replace" && (
690
- <>
691
- <span style={{ fontStyle: "italic" }}>
692
- {pathToString(patch.path)}
693
- </span>
694
- {" = "}
695
- <ObjectInspector data={patch.value} theme={theme} />
696
- </>
697
- )}
698
- {patch.op === "remove" && (
699
- <>
700
- <span style={{ color: theme.OBJECT_VALUE_STRING_COLOR }}>
701
- delete{" "}
702
- </span>
703
- <span style={{ fontStyle: "italic" }}>
704
- {pathToString(patch.path)}
705
- </span>
706
- </>
707
- )}
708
- {patch.op === "move" && (
709
- <>
710
- <span style={{ fontStyle: "italic" }}>
711
- {pathToString(patch.from!)}
712
- </span>
713
- {" -> "}
714
- <span style={{ fontStyle: "italic" }}>
715
- {pathToString(patch.path)}
716
- </span>
717
- </>
718
- )}
719
- </pre>
720
- ))}
721
- </div>
722
- ))}
723
- </div>
724
- </DisclosureSection>
725
- <DisclosureSection
366
+ historySnapshot={historySnapshot}
367
+ multiplayerStateManager={multiplayerStateManager}
368
+ />
369
+ <StateInspectorDisclosureSection
726
370
  open={showAssets}
727
371
  setOpen={setShowAssets}
728
372
  title={
729
- <TitleLabel>
730
- Assets
373
+ <StateInspectorTitleLabel>
374
+ Assets ({assets.length})
731
375
  <ColoredDot type={assetsInitialized ? "success" : "error"} />
732
- </TitleLabel>
376
+ </StateInspectorTitleLabel>
733
377
  }
734
378
  colorScheme={colorScheme}
735
379
  right={
736
380
  <span style={{ display: "flex", gap: "10px" }}>
737
- <SmallButton
381
+ <StateInspectorButton
382
+ theme={theme}
383
+ onClick={async () => {
384
+ const ok = confirm(
385
+ "Are you sure you want to delete all assets?"
386
+ );
387
+ if (!ok) return;
388
+ await Promise.all(
389
+ assets.map((asset) => assetManager.delete(asset.id))
390
+ );
391
+ }}
392
+ >
393
+ Delete all
394
+ </StateInspectorButton>
395
+ <StateInspectorButton
738
396
  theme={theme}
739
397
  onClick={() => {
740
398
  console.info(
@@ -745,8 +403,8 @@ export const StateInspector = memoGeneric(function StateInspector<
745
403
  }}
746
404
  >
747
405
  Reload
748
- </SmallButton>
749
- <SmallButton
406
+ </StateInspectorButton>
407
+ <StateInspectorButton
750
408
  theme={theme}
751
409
  onClick={() => {
752
410
  // get file input
@@ -767,36 +425,129 @@ export const StateInspector = memoGeneric(function StateInspector<
767
425
  }}
768
426
  >
769
427
  Upload
770
- </SmallButton>
428
+ </StateInspectorButton>
771
429
  </span>
772
430
  }
773
431
  >
774
- <div style={styles.sectionInner}>
432
+ <StateInspectorDisclosureRowInner>
775
433
  {assets.map((asset) => (
776
- <InspectorRow key={asset.id} colorScheme={colorScheme}>
434
+ <StateInspectorRow key={asset.id} colorScheme={colorScheme}>
777
435
  <ObjectInspector name={asset.id} data={asset} theme={theme} />
778
- <SmallButton
436
+ <StateInspectorButton
779
437
  theme={theme}
780
438
  onClick={() => assetManager.delete(asset.id)}
781
439
  >
782
440
  Delete
783
- </SmallButton>
784
- </InspectorRow>
441
+ </StateInspectorButton>
442
+ </StateInspectorRow>
443
+ ))}
444
+ </StateInspectorDisclosureRowInner>
445
+ </StateInspectorDisclosureSection>
446
+ <StateInspectorDisclosureSection
447
+ title={
448
+ <StateInspectorTitleLabel>
449
+ Resources ({resources.length})
450
+ <ColoredDot type={resourcesInitialized ? "success" : "error"} />
451
+ </StateInspectorTitleLabel>
452
+ }
453
+ colorScheme={colorScheme}
454
+ open={showResources}
455
+ setOpen={setShowResources}
456
+ right={
457
+ <span style={{ display: "flex", gap: "10px" }}>
458
+ <StateInspectorButton
459
+ theme={theme}
460
+ onClick={async () => {
461
+ const ok = confirm(
462
+ "Are you sure you want to delete all resources?"
463
+ );
464
+ if (!ok) return;
465
+ await Promise.all(
466
+ resources.map((resource) =>
467
+ resourceManager.deleteResource({ id: resource.id })
468
+ )
469
+ );
470
+ }}
471
+ >
472
+ Delete all
473
+ </StateInspectorButton>
474
+ <StateInspectorButton
475
+ theme={theme}
476
+ onClick={async () => {
477
+ const path = prompt("Enter directory path") || uuid();
478
+ await resourceManager.createResource({
479
+ type: "directory",
480
+ path,
481
+ });
482
+ }}
483
+ >
484
+ Create Directory
485
+ </StateInspectorButton>
486
+ <StateInspectorButton
487
+ theme={theme}
488
+ onClick={async () => {
489
+ const file = await uploadFile();
490
+
491
+ resourceManager.createResource({
492
+ type: "asset",
493
+ path: file.name || uuid(),
494
+ asset: {
495
+ content: Base64.encode(await file.arrayBuffer()),
496
+ contentType: file.type,
497
+ encoding: "base64",
498
+ },
499
+ });
500
+ }}
501
+ >
502
+ Create Asset
503
+ </StateInspectorButton>
504
+ </span>
505
+ }
506
+ >
507
+ <StateInspectorDisclosureRowInner>
508
+ {resources?.map((resource) => (
509
+ <StateInspectorRow key={resource.id} colorScheme={colorScheme}>
510
+ <ObjectInspector
511
+ name={resource.path}
512
+ data={resource}
513
+ theme={theme}
514
+ />
515
+ <div style={{ display: "flex", gap: "4px" }}>
516
+ <StateInspectorButton
517
+ theme={theme}
518
+ onClick={() =>
519
+ resourceManager.deleteResource({ id: resource.id })
520
+ }
521
+ >
522
+ Delete
523
+ </StateInspectorButton>
524
+ <StateInspectorButton
525
+ theme={theme}
526
+ onClick={() => {
527
+ const path = prompt("Enter new path", resource.path);
528
+ if (!path) return;
529
+ resourceManager.updateResource({ id: resource.id, path });
530
+ }}
531
+ >
532
+ Rename
533
+ </StateInspectorButton>
534
+ </div>
535
+ </StateInspectorRow>
785
536
  ))}
786
- </div>
787
- </DisclosureSection>
788
- <DisclosureSection
537
+ </StateInspectorDisclosureRowInner>
538
+ </StateInspectorDisclosureSection>
539
+ <StateInspectorDisclosureSection
789
540
  title={
790
- <TitleLabel>
541
+ <StateInspectorTitleLabel>
791
542
  Secrets
792
543
  <ColoredDot type={secretsInitialized ? "success" : "error"} />
793
- </TitleLabel>
544
+ </StateInspectorTitleLabel>
794
545
  }
795
546
  colorScheme={colorScheme}
796
547
  open={showSecrets}
797
548
  setOpen={setShowSecrets}
798
549
  right={
799
- <SmallButton
550
+ <StateInspectorButton
800
551
  theme={theme}
801
552
  onClick={() => {
802
553
  const name = prompt("Enter secret name");
@@ -807,39 +558,39 @@ export const StateInspector = memoGeneric(function StateInspector<
807
558
  }}
808
559
  >
809
560
  Create
810
- </SmallButton>
561
+ </StateInspectorButton>
811
562
  }
812
563
  >
813
- <div style={styles.sectionInner}>
564
+ <StateInspectorDisclosureRowInner>
814
565
  {secrets.map((secret) => (
815
- <InspectorRow key={secret.id} colorScheme={colorScheme}>
566
+ <StateInspectorRow key={secret.id} colorScheme={colorScheme}>
816
567
  <ObjectInspector data={secret} theme={theme} />
817
- <SmallButton
568
+ <StateInspectorButton
818
569
  theme={theme}
819
570
  onClick={() => secretManager.deleteSecret(secret.id)}
820
571
  >
821
572
  Delete
822
- </SmallButton>
823
- </InspectorRow>
573
+ </StateInspectorButton>
574
+ </StateInspectorRow>
824
575
  ))}
825
- </div>
826
- </DisclosureSection>{" "}
827
- <DisclosureSection
576
+ </StateInspectorDisclosureRowInner>
577
+ </StateInspectorDisclosureSection>{" "}
578
+ <StateInspectorDisclosureSection
828
579
  open={showInputs}
829
580
  setOpen={setShowInputs}
830
581
  title={
831
- <TitleLabel>
582
+ <StateInspectorTitleLabel>
832
583
  Inputs
833
584
  <ColoredDot type={inputsInitialized ? "success" : "error"} />
834
- </TitleLabel>
585
+ </StateInspectorTitleLabel>
835
586
  }
836
587
  colorScheme={colorScheme}
837
588
  >
838
- <div style={styles.sectionInner}>
589
+ <StateInspectorDisclosureRowInner>
839
590
  {inputs?.map((input: Input) => (
840
- <InspectorRow key={input.id} colorScheme={colorScheme}>
591
+ <StateInspectorRow key={input.id} colorScheme={colorScheme}>
841
592
  <ObjectInspector data={input} theme={theme} />
842
- </InspectorRow>
593
+ </StateInspectorRow>
843
594
  ))}
844
595
  {!inputs?.length && (
845
596
  <div
@@ -854,26 +605,26 @@ export const StateInspector = memoGeneric(function StateInspector<
854
605
  <span>No inputs</span>
855
606
  </div>
856
607
  )}
857
- </div>
858
- </DisclosureSection>
859
- <DisclosureSection
608
+ </StateInspectorDisclosureRowInner>
609
+ </StateInspectorDisclosureSection>
610
+ <StateInspectorDisclosureSection
860
611
  open={showOutputTransforms}
861
612
  setOpen={setShowOutputTransforms}
862
613
  title={
863
- <TitleLabel>
614
+ <StateInspectorTitleLabel>
864
615
  Output Transforms
865
616
  <ColoredDot
866
617
  type={outputTransformsInitialized ? "success" : "error"}
867
618
  />
868
- </TitleLabel>
619
+ </StateInspectorTitleLabel>
869
620
  }
870
621
  colorScheme={colorScheme}
871
622
  >
872
- <div style={styles.sectionInner}>
623
+ <StateInspectorDisclosureRowInner>
873
624
  {outputTransforms?.map((transform: OutputTransform) => (
874
- <InspectorRow key={transform.id} colorScheme={colorScheme}>
625
+ <StateInspectorRow key={transform.id} colorScheme={colorScheme}>
875
626
  <ObjectInspector data={transform} theme={theme} />
876
- </InspectorRow>
627
+ </StateInspectorRow>
877
628
  ))}
878
629
  {!outputTransforms?.length && (
879
630
  <div
@@ -888,17 +639,17 @@ export const StateInspector = memoGeneric(function StateInspector<
888
639
  <span>No output transforms</span>
889
640
  </div>
890
641
  )}
891
- </div>
892
- </DisclosureSection>
893
- <DisclosureSection
642
+ </StateInspectorDisclosureRowInner>
643
+ </StateInspectorDisclosureSection>
644
+ <StateInspectorDisclosureSection
894
645
  title="Tasks"
895
646
  colorScheme={colorScheme}
896
647
  open={showTasks}
897
648
  setOpen={setShowTasks}
898
649
  >
899
- <div style={styles.sectionInner}>
650
+ <StateInspectorDisclosureRowInner>
900
651
  {tasks?.map((task) => (
901
- <InspectorRow
652
+ <StateInspectorRow
902
653
  key={task.id}
903
654
  colorScheme={colorScheme}
904
655
  style={{
@@ -915,269 +666,43 @@ export const StateInspector = memoGeneric(function StateInspector<
915
666
  data={task.payload}
916
667
  theme={theme}
917
668
  />
918
- </InspectorRow>
669
+ </StateInspectorRow>
919
670
  ))}
920
- </div>
921
- </DisclosureSection>
922
- <DisclosureSection
671
+ </StateInspectorDisclosureRowInner>
672
+ </StateInspectorDisclosureSection>
673
+ <StateInspectorDisclosureSection
923
674
  open={showEphemeral}
924
675
  setOpen={setShowEphemeral}
925
676
  title="Ephemeral User Data"
926
677
  colorScheme={colorScheme}
927
678
  >
928
- <div style={styles.sectionInner}>
679
+ <StateInspectorDisclosureRowInner>
929
680
  {Object.entries(ephemeral).map(([key, value]) => (
930
- <InspectorRow key={key} colorScheme={colorScheme}>
681
+ <StateInspectorRow key={key} colorScheme={colorScheme}>
931
682
  <ObjectInspector
932
683
  name={key}
933
684
  data={value}
934
685
  theme={theme}
935
686
  expandLevel={10}
936
687
  />
937
- </InspectorRow>
688
+ </StateInspectorRow>
938
689
  ))}
939
- </div>
940
- </DisclosureSection>
941
- <DisclosureSection
942
- open={showEvents}
943
- setOpen={setShowEvents}
944
- title="Events"
690
+ </StateInspectorDisclosureRowInner>
691
+ </StateInspectorDisclosureSection>
692
+ <ActivityEventsSection
945
693
  colorScheme={colorScheme}
946
- >
947
- <div ref={eventsContainerRef} style={styles.sectionInner}>
948
- {connectionEvents?.map((event, index) =>
949
- event.type === "stateChange" ? (
950
- <InspectorRow key={index} colorScheme={colorScheme}>
951
- connection:{" "}
952
- <span style={{ fontStyle: "italic" }}>
953
- {event.state.toLowerCase()}
954
- </span>
955
- </InspectorRow>
956
- ) : (
957
- <InspectorRow
958
- key={index}
959
- colorScheme={colorScheme}
960
- variant={event.type === "send" ? "up" : "down"}
961
- style={{
962
- padding: "0px 12px 1px",
963
- }}
964
- >
965
- <ObjectInspector
966
- data={event.type === "error" ? event.error : event.message}
967
- theme={theme}
968
- nodeRenderer={({
969
- depth,
970
- name,
971
- data,
972
- isNonenumerable,
973
- }: any) => {
974
- const direction = event.type === "send" ? "up" : "down";
975
-
976
- return depth === 0 ? (
977
- <ObjectRootLabel direction={direction} data={data} />
978
- ) : (
979
- <ObjectLabel
980
- direction={direction}
981
- name={name}
982
- data={data}
983
- isNonenumerable={isNonenumerable}
984
- />
985
- );
986
- }}
987
- />
988
- </InspectorRow>
989
- )
990
- )}
991
- {!connectionEvents && (
992
- <div
993
- style={{
994
- padding: "12px",
995
- fontSize: "12px",
996
- display: "flex",
997
- flexDirection: "column",
998
- gap: "4px",
999
- }}
1000
- >
1001
- <span>No recorded events</span>
1002
- </div>
1003
- )}
1004
- </div>
1005
- </DisclosureSection>
694
+ activityEventsContainerRef={activityEventsContainerRef}
695
+ showEvents={showActivityEvents}
696
+ setShowEvents={setShowActivityEvents}
697
+ activityEventsManager={activityEventsManager}
698
+ />
699
+ <EventsSection<S>
700
+ connectionEvents={connectionEvents}
701
+ colorScheme={colorScheme}
702
+ eventsContainerRef={eventsContainerRef}
703
+ showEvents={showEvents}
704
+ setShowEvents={setShowEvents}
705
+ />
1006
706
  </div>
1007
707
  );
1008
708
  });
1009
-
1010
- type MessageDirection = "up" | "down";
1011
-
1012
- const ObjectRootLabel: FC<{
1013
- name?: string;
1014
- data: any;
1015
- direction?: MessageDirection;
1016
- }> = ({ name, data, direction }) => {
1017
- if (typeof name === "string") {
1018
- return (
1019
- <span>
1020
- <ObjectName name={name} />
1021
- <span>: </span>
1022
- <ObjectPreview data={data} />
1023
- </span>
1024
- );
1025
- }
1026
- if (direction === "up" || direction === "down") {
1027
- const arrow = direction === "up" ? "↑" : "↓";
1028
-
1029
- return (
1030
- <span>
1031
- <span
1032
- style={{
1033
- background:
1034
- direction === "up" ? "rgba(0,255,0,0.2)" : "rgba(255,0,0,0.2)",
1035
- // color: "white",
1036
- width: 12,
1037
- height: 12,
1038
- borderRadius: 2,
1039
- display: "inline-flex",
1040
- justifyContent: "center",
1041
- alignItems: "center",
1042
- marginRight: 4,
1043
- lineHeight: "12px",
1044
- }}
1045
- >
1046
- {arrow}
1047
- </span>
1048
- <ObjectPreview data={data} />
1049
- </span>
1050
- );
1051
- } else {
1052
- return <ObjectPreview data={data} />;
1053
- }
1054
- };
1055
-
1056
- // Rotates ▶️ or ▼ based on expanded state
1057
- function Arrow({
1058
- expanded,
1059
- onClick,
1060
- style,
1061
- }: {
1062
- expanded: boolean;
1063
- onClick?: () => void;
1064
- style?: CSSProperties;
1065
- }) {
1066
- return (
1067
- <span
1068
- role="button"
1069
- onClick={onClick}
1070
- style={{
1071
- display: "inline-block",
1072
- textAlign: "center",
1073
- transform: expanded ? "rotate(0deg)" : "rotate(-90deg)",
1074
- fontFamily: "Menlo, monospace",
1075
- ...style,
1076
- }}
1077
- >
1078
- {"▼"}
1079
- </span>
1080
- );
1081
- }
1082
-
1083
- function pathToString(extendedPath: ExtendedPathKey[]) {
1084
- return extendedPath
1085
- .map((key) =>
1086
- typeof key === "object"
1087
- ? `:${ellipsis(key.id.toString(), 8, "middle")}`
1088
- : key
1089
- )
1090
- .map((key, index) =>
1091
- index === 0 || (typeof key === "string" && key.startsWith(":"))
1092
- ? key
1093
- : `.${key}`
1094
- )
1095
- .join("");
1096
- }
1097
-
1098
- type EllipsisPosition = "start" | "middle" | "end";
1099
-
1100
- function ellipsis(
1101
- str: string,
1102
- maxLength: number,
1103
- position: EllipsisPosition = "end"
1104
- ) {
1105
- if (str.length <= maxLength) return str;
1106
-
1107
- switch (position) {
1108
- case "start":
1109
- return `…${str.slice(str.length - maxLength)}`;
1110
- case "middle":
1111
- const halfLength = Math.floor(maxLength / 2);
1112
- return `${str.slice(0, halfLength)}…${str.slice(str.length - halfLength)}`;
1113
- case "end":
1114
- return `${str.slice(0, maxLength)}…`;
1115
- }
1116
- }
1117
-
1118
- function SmallButton({
1119
- children,
1120
- onClick,
1121
- style,
1122
- theme,
1123
- disabled,
1124
- }: {
1125
- children: React.ReactNode;
1126
- onClick: () => void;
1127
- style?: CSSProperties;
1128
- theme: typeof chromeLight;
1129
- disabled?: boolean;
1130
- }) {
1131
- return (
1132
- <button
1133
- type="button"
1134
- disabled={disabled}
1135
- onClick={(event) => {
1136
- event.stopPropagation();
1137
- onClick();
1138
- }}
1139
- style={{
1140
- flex: "0",
1141
- appearance: "none",
1142
- background: "none",
1143
- color: theme.BASE_COLOR,
1144
- opacity: disabled ? 0.25 : 1,
1145
- border: "none",
1146
- fontSize: "12px",
1147
- whiteSpace: "nowrap",
1148
- display: "inline-flex",
1149
- alignItems: "center",
1150
- justifyContent: "center",
1151
- padding: "0",
1152
- cursor: "pointer",
1153
- ...style,
1154
- }}
1155
- >
1156
- {children}
1157
- </button>
1158
- );
1159
- }
1160
-
1161
- function ColoredDot({ type }: { type: "success" | "error" }) {
1162
- return (
1163
- <span
1164
- style={{
1165
- display: "inline-block",
1166
- width: 6,
1167
- height: 6,
1168
- background:
1169
- type === "success" ? "rgba(0,200,0,0.8)" : "rgba(200,0,0,0.8)",
1170
- borderRadius: "50%",
1171
- verticalAlign: "middle",
1172
- }}
1173
- />
1174
- );
1175
- }
1176
-
1177
- function TitleLabel({ children }: { children: React.ReactNode }) {
1178
- return (
1179
- <span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
1180
- {children}
1181
- </span>
1182
- );
1183
- }