@opengeni/react 5.2.0-canary.1 → 6.0.0-canary.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "5.2.0-canary.1",
3
+ "version": "6.0.0-canary.0",
4
4
  "description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -133,8 +133,8 @@
133
133
  "@dnd-kit/utilities": "3.2.2",
134
134
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
135
135
  "@fontsource-variable/noto-sans-jp": "5.2.10",
136
- "@opengeni/connect": "^0.2.0-canary.8",
137
- "@opengeni/sdk": "^5.1.0-canary.2",
136
+ "@opengeni/connect": "^0.2.0-canary.9",
137
+ "@opengeni/sdk": "^6.0.0-canary.0",
138
138
  "clsx": "^2.1.1",
139
139
  "dequal": "2.0.3",
140
140
  "lucide-react": "^1.8.0",
package/src/artifacts.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { inlineHtmlDocument } from "./components/artifacts/inline-html-document";
2
- export { useRetainedImageObjectUrl } from "./timeline/retained-image";
2
+ export { isRetainedImageContentType, useRetainedImageObjectUrl } from "./timeline/retained-image";
3
3
  /** Public artifact surfaces. Import from `@opengeni/react/artifacts`. */
4
4
  export {
5
5
  ArtifactSurface,
@@ -2,6 +2,10 @@ import { RollingActivity } from "../timeline/rolling-activity";
2
2
  import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
3
3
  import { ChildSessionLink } from "./child-session-link";
4
4
  import { useStartupDetails } from "../timeline/startup-preference";
5
+ import { parseSandboxFileArtifactReceipt } from "@opengeni/sdk";
6
+ import { unwrapMcpOutput } from "../timeline/parsers";
7
+ import { isRetainedImageContentType } from "../timeline/retained-image";
8
+ import { mcpToolLeaf } from "../timeline/tool-display-name";
5
9
  import type {
6
10
  DraftTimelineAnnotation,
7
11
  MediaGenerationResult,
@@ -2537,9 +2541,15 @@ const TimelineGroupView = memo(function TimelineGroupView({
2537
2541
  // Settled (or live-fold) activity clusters get a chip. Inside an expanded
2538
2542
  // turn that is the second layer — quiet nested chips under the outer turn
2539
2543
  // summary when contiguous activity naturally clusters (≥2 only).
2544
+ const containsPresentedImage = timelineGroupContainsPresentedImage(group);
2545
+ const hasRememberedImageFold =
2546
+ group.kind === "activity" && containsPresentedImage && foldMemory?.has(group.id);
2547
+ // Primary images stay visible through live narration and the turn wrap.
2548
+ // Manual collapse still belongs to TurnSummary's existing fold memory.
2540
2549
  const activityShouldFold =
2541
- group.kind === "activity" && !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2542
- const containsGeneratedImage = timelineGroupContainsGeneratedImage(group);
2550
+ group.kind === "activity" &&
2551
+ !containsPresentedImage &&
2552
+ !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2543
2553
  // Latch live→folded so a top-level shell that was already mounted open can
2544
2554
  // start the settle beat without remounting bare rail → wrapper.
2545
2555
  const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn);
@@ -2548,7 +2558,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2548
2558
  group.kind === "turn" &&
2549
2559
  (group.outcome === "failed" ||
2550
2560
  timelineGroupContainsAuthNeeded(group) ||
2551
- containsGeneratedImage);
2561
+ containsPresentedImage);
2552
2562
  // activity-* → turn-* remount: carry resting state so settleFold does not
2553
2563
  // re-open a chip the reader already watched collapse.
2554
2564
  if (group.kind === "turn" && foldMemory && !insideTurn) {
@@ -2589,6 +2599,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2589
2599
  if (
2590
2600
  turnSummary?.rolling &&
2591
2601
  !startupDetails &&
2602
+ !hasRememberedImageFold &&
2592
2603
  group.items.filter((item) => item.kind !== "startup-phase").length === 1
2593
2604
  ) {
2594
2605
  return (
@@ -2620,8 +2631,12 @@ const TimelineGroupView = memo(function TimelineGroupView({
2620
2631
  !startupDetails &&
2621
2632
  visibleActivity.length === 1 &&
2622
2633
  visibleActivity[0]?.kind === "reasoning";
2634
+ // Untouched images stay primary output, but a reader-owned image fold
2635
+ // must keep its shell across a multi-cluster wrap. A bare rail would
2636
+ // bypass the remembered choice; retaining either state also lets the
2637
+ // reader reopen and close the same chip after settlement.
2623
2638
  const useNestedChip =
2624
- nestClusterChips && activityShouldFold && !containsGeneratedImage && !singleThought;
2639
+ nestClusterChips && (activityShouldFold || hasRememberedImageFold) && !singleThought;
2625
2640
  if (!useNestedChip) {
2626
2641
  return (
2627
2642
  <ActivityRail
@@ -2663,16 +2678,24 @@ const TimelineGroupView = memo(function TimelineGroupView({
2663
2678
  // flips settleFold (collapse) instead of remounting bare rail → wrapper.
2664
2679
  return (
2665
2680
  <TurnSummary
2681
+ // Apply the primary-output default when a rolling activity first
2682
+ // produces an image; the stable foldKey still preserves user choices.
2683
+ key={containsPresentedImage ? "primary-image" : "activity"}
2666
2684
  items={group.items}
2667
2685
  outcome={group.outcome}
2668
2686
  failureText={group.failureText}
2669
2687
  defaultOpen={
2670
- group.outcome === "failed" || (!turnSummary?.rolling && !activityShouldFold)
2688
+ group.outcome === "failed" ||
2689
+ containsPresentedImage ||
2690
+ (!turnSummary?.rolling && !activityShouldFold)
2671
2691
  ? true
2672
2692
  : undefined
2673
2693
  }
2674
2694
  liveHeader={
2675
- turnSummary?.rolling && !group.outcome && !foldLiveCluster ? (
2695
+ turnSummary?.rolling &&
2696
+ !containsPresentedImage &&
2697
+ !group.outcome &&
2698
+ !foldLiveCluster ? (
2676
2699
  <RollingActivity
2677
2700
  items={group.items}
2678
2701
  toolRegistry={toolRegistry}
@@ -2862,17 +2885,24 @@ function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean {
2862
2885
  }
2863
2886
  }
2864
2887
 
2865
- /** Generated images are primary user-visible output, not incidental activity. */
2866
- function timelineGroupContainsGeneratedImage(group: TimelineGroup): boolean {
2888
+ /** Deliberately published images are primary output, not incidental screenshots. */
2889
+ function timelineGroupContainsPresentedImage(group: TimelineGroup): boolean {
2867
2890
  switch (group.kind) {
2868
2891
  case "item":
2869
2892
  return false;
2870
2893
  case "activity":
2871
- return group.items.some(
2872
- (item) => item.kind === "tool-call" && item.name === "generate_image",
2873
- );
2894
+ return group.items.some((item) => {
2895
+ if (item.kind !== "tool-call") return false;
2896
+ const name = mcpToolLeaf(item.name);
2897
+ if (name === "generate_image" || name === "image_generation_call") return true;
2898
+ if (item.status !== "complete" || name !== "sandbox_file_publish") return false;
2899
+ const output = unwrapMcpOutput(item.output);
2900
+ if (output.isError) return false;
2901
+ const receipt = parseSandboxFileArtifactReceipt(output.text);
2902
+ return receipt !== null && isRetainedImageContentType(receipt.artifact.contentType);
2903
+ });
2874
2904
  case "turn":
2875
- return group.groups.some(timelineGroupContainsGeneratedImage);
2905
+ return group.groups.some(timelineGroupContainsPresentedImage);
2876
2906
  }
2877
2907
  }
2878
2908
 
@@ -26,22 +26,11 @@ export type ConnectionCatalogProps = {
26
26
  emptyMessage?: string;
27
27
  };
28
28
 
29
- function ConnectionDetailsAction({
30
- option,
31
- name = option.name,
32
- }: {
33
- option: ConnectionCatalogOption;
34
- name?: string;
35
- }) {
29
+ function ConnectionDetailsIndicator() {
36
30
  return (
37
- <button
38
- type="button"
39
- className="og-connection-details-action"
40
- aria-label={`View ${name} details`}
41
- onClick={option.onOpen}
42
- >
43
- <span aria-hidden>{option.connected ? "›" : "+"}</span>
44
- </button>
31
+ <span aria-hidden className="og-connection-group-chevron">
32
+
33
+ </span>
45
34
  );
46
35
  }
47
36
 
@@ -54,10 +43,9 @@ export function ConnectionOptionRow({ option }: { option: ConnectionCatalogOptio
54
43
  <span>{option.description}</span>
55
44
  </span>
56
45
  <span className="og-connection-catalog-status">{option.status}</span>
46
+ {!option.action ? <ConnectionDetailsIndicator /> : null}
57
47
  </button>
58
- <span className="og-connection-option-action">
59
- {option.action ?? <ConnectionDetailsAction option={option} />}
60
- </span>
48
+ {option.action ? <span className="og-connection-option-action">{option.action}</span> : null}
61
49
  </div>
62
50
  );
63
51
  }
@@ -91,10 +79,9 @@ export function ConnectionServiceRow({ service }: { service: ConnectionCatalogSe
91
79
  onClick={service.options[0]!.onOpen}
92
80
  >
93
81
  {identity}
82
+ {!service.options[0]?.action ? <ConnectionDetailsIndicator /> : null}
94
83
  </button>
95
- {service.options[0]?.action ?? (
96
- <ConnectionDetailsAction option={service.options[0]!} name={service.name} />
97
- )}
84
+ {service.options[0]?.action}
98
85
  </div>
99
86
  ) : (
100
87
  <details className="og-connection-catalog-service">
@@ -7,6 +7,18 @@ type RetainedImageState =
7
7
  | { kind: "unavailable"; label: string }
8
8
  | { kind: "error"; message: string };
9
9
 
10
+ /** Shared browser preview allowlist; other published files remain downloads. */
11
+ export function isRetainedImageContentType(contentType: string): boolean {
12
+ return [
13
+ "image/png",
14
+ "image/jpeg",
15
+ "image/gif",
16
+ "image/webp",
17
+ "image/avif",
18
+ "image/svg+xml",
19
+ ].includes(contentType);
20
+ }
21
+
10
22
  export function useRetainedImageObjectUrl(
11
23
  artifact: RetainedArtifactReference,
12
24
  load: ToolRendererProps["loadRetainedArtifact"],
@@ -1,5 +1,5 @@
1
1
  import { KnowledgeReceiptRow } from "./knowledge-receipt";
2
- import { useRetainedImageObjectUrl } from "./retained-image";
2
+ import { isRetainedImageContentType, useRetainedImageObjectUrl } from "./retained-image";
3
3
  import {
4
4
  parseSandboxFileArtifactReceipt,
5
5
  type GitFileDiff,
@@ -783,9 +783,12 @@ function RetainedSessionImageDisclosure({
783
783
  batched,
784
784
  failed,
785
785
  cancelled,
786
+ filename,
787
+ defaultOpen,
788
+ children,
786
789
  }: {
787
790
  artifact: RetainedArtifactReference;
788
- load: ToolRendererProps["loadRetainedScreenshot"];
791
+ load: ToolRendererProps["loadRetainedArtifact"];
789
792
  title: string;
790
793
  caption: string;
791
794
  noun: "image" | "screenshot";
@@ -794,15 +797,19 @@ function RetainedSessionImageDisclosure({
794
797
  batched: string | null;
795
798
  failed: boolean;
796
799
  cancelled: boolean;
800
+ filename?: string;
801
+ defaultOpen?: boolean;
802
+ children?: ReactNode;
797
803
  }) {
798
804
  const state = useRetainedImageObjectUrl(artifact, load);
799
- const downloadFilename = retainedImageFilename(artifact);
805
+ const downloadFilename = filename ?? retainedImageFilename(artifact);
800
806
 
801
807
  return (
802
808
  <ActivityDisclosure
803
809
  icon={icon}
804
810
  iconTone={failed ? "failed" : state.kind === "ready" ? "accent" : "muted"}
805
811
  title={title}
812
+ defaultOpen={defaultOpen}
806
813
  failed={failed}
807
814
  cancelled={cancelled}
808
815
  preview={
@@ -852,6 +859,7 @@ function RetainedSessionImageDisclosure({
852
859
  </BodyNote>
853
860
  )}
854
861
  {batched ? <BodyNote>batched: {batched}</BodyNote> : null}
862
+ {children}
855
863
  </ActivityDisclosure>
856
864
  );
857
865
  }
@@ -945,6 +953,22 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
945
953
  return <GenericRenderer item={item} />;
946
954
  }
947
955
 
956
+ // The closed SDK receipt has already checked the workspace-qualified route.
957
+ const workspaceId = /^\/v1\/workspaces\/([0-9a-f-]+)\/artifacts\//.exec(
958
+ receipt.artifact.retrieval.path,
959
+ )?.[1];
960
+ const openLink = workspaceId ? (
961
+ <a
962
+ href={`/workspaces/${workspaceId}/artifacts/files/${receipt.artifact.artifactId}`}
963
+ aria-label={`Open ${receipt.filename} in Artifacts`}
964
+ className="inline-flex min-h-7 items-center rounded-og-sm px-2 text-og-sm font-medium text-og-accent-strong hover:bg-og-surface-2 hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-10"
965
+ onClick={(event) => event.stopPropagation()}
966
+ onKeyDown={(event) => event.stopPropagation()}
967
+ >
968
+ Open in Artifacts
969
+ </a>
970
+ ) : null;
971
+
948
972
  const download = async () => {
949
973
  if (!loadRetainedArtifact) {
950
974
  setDownloadState("error");
@@ -977,6 +1001,44 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
977
1001
  }
978
1002
  };
979
1003
 
1004
+ const downloadButton = (
1005
+ <button
1006
+ type="button"
1007
+ onClick={() => void download()}
1008
+ disabled={downloadState === "loading"}
1009
+ className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-surface-2 disabled:cursor-wait disabled:opacity-60"
1010
+ >
1011
+ <DownloadIcon className="size-3.5" />
1012
+ {downloadState === "loading"
1013
+ ? "Preparing…"
1014
+ : downloadState === "error"
1015
+ ? "Retry download"
1016
+ : "Download"}
1017
+ </button>
1018
+ );
1019
+
1020
+ if (isRetainedImageContentType(receipt.artifact.contentType)) {
1021
+ return (
1022
+ <RetainedSessionImageDisclosure
1023
+ artifact={receipt.artifact}
1024
+ load={loadRetainedArtifact}
1025
+ title={`Published ${receipt.filename}`}
1026
+ caption={receipt.filename}
1027
+ noun="image"
1028
+ icon={<ImageIcon className={ICON_SIZE} />}
1029
+ lightboxLabel="Image"
1030
+ batched={null}
1031
+ failed={false}
1032
+ cancelled={false}
1033
+ filename={receipt.filename}
1034
+ defaultOpen
1035
+ >
1036
+ {downloadButton}
1037
+ {openLink}
1038
+ </RetainedSessionImageDisclosure>
1039
+ );
1040
+ }
1041
+
980
1042
  return (
981
1043
  <ActivityDisclosure
982
1044
  icon={<DownloadIcon className={ICON_SIZE} />}
@@ -985,19 +1047,8 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
985
1047
  defaultOpen
986
1048
  preview={formatBytes(receipt.artifact.originalBytes)}
987
1049
  >
988
- <button
989
- type="button"
990
- onClick={() => void download()}
991
- disabled={downloadState === "loading"}
992
- className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-surface-2 disabled:cursor-wait disabled:opacity-60"
993
- >
994
- <DownloadIcon className="size-3.5" />
995
- {downloadState === "loading"
996
- ? "Preparing…"
997
- : downloadState === "error"
998
- ? "Retry download"
999
- : "Download"}
1000
- </button>
1050
+ {downloadButton}
1051
+ {openLink}
1001
1052
  </ActivityDisclosure>
1002
1053
  );
1003
1054
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/timeline/retained-image.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { RetainedArtifactReference } from \"@opengeni/sdk\";\nimport type { ToolRendererProps } from \"./registry\";\ntype RetainedImageState =\n | { kind: \"loading\" }\n | { kind: \"ready\"; url: string }\n | { kind: \"unavailable\"; label: string }\n | { kind: \"error\"; message: string };\n\nexport function useRetainedImageObjectUrl(\n artifact: RetainedArtifactReference,\n load: ToolRendererProps[\"loadRetainedArtifact\"],\n): RetainedImageState {\n const [state, setState] = useState<RetainedImageState>({ kind: \"loading\" });\n // Function outputs are commonly serialized JSON. Parsing them creates a new\n // object on every render, so depend on the immutable wire value rather than\n // object identity; otherwise the loader can refetch after its own setState.\n const artifactValue = retainedArtifactValue(artifact);\n const stableArtifactRef = useRef({ value: artifactValue, artifact });\n if (stableArtifactRef.current.value !== artifactValue) {\n stableArtifactRef.current = { value: artifactValue, artifact };\n }\n const stableArtifact = stableArtifactRef.current.artifact;\n useEffect(() => {\n if (!load) {\n setState({ kind: \"unavailable\", label: \"retrieval is not configured\" });\n return;\n }\n const controller = new AbortController();\n let objectUrl: string | null = null;\n setState({ kind: \"loading\" });\n void load(stableArtifact, controller.signal)\n .then((source) => {\n if (controller.signal.aborted) return;\n if (!source) {\n setState({ kind: \"unavailable\", label: \"bytes are unavailable\" });\n return;\n }\n if (!(source instanceof Uint8Array)) {\n if (!source.url.trim()) {\n setState({ kind: \"unavailable\", label: \"URL is unavailable\" });\n return;\n }\n setState({ kind: \"ready\", url: source.url });\n return;\n }\n objectUrl = URL.createObjectURL(\n new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),\n );\n setState({ kind: \"ready\", url: objectUrl });\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n const status =\n error && typeof error === \"object\" && \"status\" in error\n ? Number((error as { status?: unknown }).status)\n : null;\n setState(\n status === 404\n ? { kind: \"unavailable\", label: \"deleted\" }\n : status === 410\n ? { kind: \"unavailable\", label: \"expired or unavailable\" }\n : { kind: \"error\", message: \"retrieval failed\" },\n );\n });\n return () => {\n controller.abort();\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [stableArtifact, load]);\n return state;\n}\n\nfunction retainedArtifactValue(artifact: RetainedArtifactReference): string {\n return [\n artifact.artifactId,\n artifact.kind,\n artifact.contentType,\n artifact.originalBytes,\n artifact.sha256,\n artifact.retainedAt,\n artifact.dimensions?.width ?? \"\",\n artifact.dimensions?.height ?? \"\",\n artifact.retention.policy,\n artifact.retention.expiresAt ?? \"\",\n artifact.retrieval.method,\n artifact.retrieval.path,\n artifact.retrieval.acceptRanges,\n artifact.retrieval.maxRangeBytes,\n ].join(\"\\0\");\n}\n"],"mappings":";AAAA,SAAS,WAAW,QAAQ,gBAAgB;AASrC,SAAS,0BACd,UACA,MACoB;AACpB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA6B,EAAE,MAAM,UAAU,CAAC;AAI1E,QAAM,gBAAgB,sBAAsB,QAAQ;AACpD,QAAM,oBAAoB,OAAO,EAAE,OAAO,eAAe,SAAS,CAAC;AACnE,MAAI,kBAAkB,QAAQ,UAAU,eAAe;AACrD,sBAAkB,UAAU,EAAE,OAAO,eAAe,SAAS;AAAA,EAC/D;AACA,QAAM,iBAAiB,kBAAkB,QAAQ;AACjD,YAAU,MAAM;AACd,QAAI,CAAC,MAAM;AACT,eAAS,EAAE,MAAM,eAAe,OAAO,8BAA8B,CAAC;AACtE;AAAA,IACF;AACA,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI,YAA2B;AAC/B,aAAS,EAAE,MAAM,UAAU,CAAC;AAC5B,SAAK,KAAK,gBAAgB,WAAW,MAAM,EACxC,KAAK,CAAC,WAAW;AAChB,UAAI,WAAW,OAAO,QAAS;AAC/B,UAAI,CAAC,QAAQ;AACX,iBAAS,EAAE,MAAM,eAAe,OAAO,wBAAwB,CAAC;AAChE;AAAA,MACF;AACA,UAAI,EAAE,kBAAkB,aAAa;AACnC,YAAI,CAAC,OAAO,IAAI,KAAK,GAAG;AACtB,mBAAS,EAAE,MAAM,eAAe,OAAO,qBAAqB,CAAC;AAC7D;AAAA,QACF;AACA,iBAAS,EAAE,MAAM,SAAS,KAAK,OAAO,IAAI,CAAC;AAC3C;AAAA,MACF;AACA,kBAAY,IAAI;AAAA,QACd,IAAI,KAAK,CAAC,MAA6B,GAAG,EAAE,MAAM,eAAe,YAAY,CAAC;AAAA,MAChF;AACA,eAAS,EAAE,MAAM,SAAS,KAAK,UAAU,CAAC;AAAA,IAC5C,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,UAAI,WAAW,OAAO,QAAS;AAC/B,YAAM,SACJ,SAAS,OAAO,UAAU,YAAY,YAAY,QAC9C,OAAQ,MAA+B,MAAM,IAC7C;AACN;AAAA,QACE,WAAW,MACP,EAAE,MAAM,eAAe,OAAO,UAAU,IACxC,WAAW,MACT,EAAE,MAAM,eAAe,OAAO,yBAAyB,IACvD,EAAE,MAAM,SAAS,SAAS,mBAAmB;AAAA,MACrD;AAAA,IACF,CAAC;AACH,WAAO,MAAM;AACX,iBAAW,MAAM;AACjB,UAAI,UAAW,KAAI,gBAAgB,SAAS;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,gBAAgB,IAAI,CAAC;AACzB,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA6C;AAC1E,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,YAAY,SAAS;AAAA,IAC9B,SAAS,YAAY,UAAU;AAAA,IAC/B,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU,aAAa;AAAA,IAChC,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,IACnB,SAAS,UAAU;AAAA,EACrB,EAAE,KAAK,IAAI;AACb;","names":[]}