@replohq/sandbox-runtime 1.7.0 → 1.7.1

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/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "branch": "HEAD",
5
- "commit": "d7576c3808fcbadef1e0cb29b3ea3ed492485a05",
6
- "builtAt": "2026-09-14T23:38:15.448Z"
5
+ "commit": "a518bd507469206a903941ac3dde233c966036de",
6
+ "builtAt": "2026-09-15T05:05:19.645Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.7.0",
6
+ "version": "1.7.1",
7
7
  "branch": "HEAD",
8
- "commit": "d7576c3808fcbadef1e0cb29b3ea3ed492485a05",
9
- "builtAt": "2026-09-14T23:38:15.448Z"
8
+ "commit": "a518bd507469206a903941ac3dde233c966036de",
9
+ "builtAt": "2026-09-15T05:05:19.645Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -5,6 +5,7 @@ import {
5
5
  } from "../iframeRpcChannel";
6
6
  import { setSelectionFrameEditing } from "../selectionFrame";
7
7
  import { symbolicateSelectedElement } from "../symbolicate";
8
+ import { createInlineTiptapEditor } from "../tiptapInlineEditor";
8
9
  import { getRepetitionSiblingElements } from "./useSiblingResolution";
9
10
  const EMPTY_ACTIVE_MARKS = {
10
11
  bold: false,
@@ -133,30 +134,21 @@ function useEditModeStateMachine({
133
134
  editingElementRef.current = element;
134
135
  editIdRef.current = editId;
135
136
  originalInnerHTMLRef.current = originalInnerHTML;
136
- void import("../tiptapInlineEditor").then(({ createInlineTiptapEditor }) => {
137
- if (editingElementRef.current !== element || editIdRef.current !== editId) {
138
- return;
139
- }
140
- editorRef.current = createInlineTiptapEditor({
141
- element,
142
- content: originalInnerHTML,
143
- onCommit: commit,
144
- onEscape: revert,
145
- onBlur: handleBlur,
146
- onSelectionUpdate: (activeMarks) => {
147
- postEditorState({ activeMarks, isEditing: true });
148
- }
149
- });
150
- initialHTMLRef.current = editorRef.current.getHTML();
151
- beginEditingAtRef.current = Date.now();
152
- editorRef.current.focusAndSelectAll();
153
- setSelectionFrameEditing(true);
154
- postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });
155
- }).catch(() => {
156
- if (editingElementRef.current === element && editIdRef.current === editId) {
157
- revert();
137
+ editorRef.current = createInlineTiptapEditor({
138
+ element,
139
+ content: originalInnerHTML,
140
+ onCommit: commit,
141
+ onEscape: revert,
142
+ onBlur: handleBlur,
143
+ onSelectionUpdate: (activeMarks) => {
144
+ postEditorState({ activeMarks, isEditing: true });
158
145
  }
159
146
  });
147
+ initialHTMLRef.current = editorRef.current.getHTML();
148
+ beginEditingAtRef.current = Date.now();
149
+ editorRef.current.focusAndSelectAll();
150
+ setSelectionFrameEditing(true);
151
+ postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });
160
152
  },
161
153
  [commit, handleBlur, revert]
162
154
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useEditModeStateMachine.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n void import(\"../tiptapInlineEditor\")\n .then(({ createInlineTiptapEditor }) => {\n if (\n editingElementRef.current !== element ||\n editIdRef.current !== editId\n ) {\n return;\n }\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n })\n .catch(() => {\n if (\n editingElementRef.current === element &&\n editIdRef.current === editId\n ) {\n revert();\n }\n });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n siblingElements: getRepetitionSiblingElements({ element }),\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { createInlineTiptapEditor } from \"../tiptapInlineEditor\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n siblingElements: getRepetitionSiblingElements({ element }),\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }