pi-studio 0.9.40 → 0.9.41
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/CHANGELOG.md +6 -0
- package/client/studio-client.js +284 -244
- package/client/studio-navigation-helpers.js +530 -0
- package/index.ts +83 -26
- package/package.json +2 -1
- package/shared/studio-tab-launcher.js +86 -0
package/index.ts
CHANGED
|
@@ -31,6 +31,12 @@ import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js"
|
|
|
31
31
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
32
|
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
33
33
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
34
|
+
import {
|
|
35
|
+
buildStudioPendingPage,
|
|
36
|
+
buildStudioPendingSecurityHeaders,
|
|
37
|
+
isValidStudioLaunchId,
|
|
38
|
+
normalizeStudioPendingKind,
|
|
39
|
+
} from "./shared/studio-tab-launcher.js";
|
|
34
40
|
import { renderStudioAnnotationInlineHtml } from "./shared/studio-annotation-render.js";
|
|
35
41
|
import {
|
|
36
42
|
buildStudioMermaidCliIconArgs,
|
|
@@ -90,6 +96,7 @@ interface StudioQuartoPreviewState {
|
|
|
90
96
|
const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
|
|
91
97
|
const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
|
|
92
98
|
const STUDIO_MERMAID_HELPERS_URL = new URL("./client/studio-mermaid-helpers.js", import.meta.url);
|
|
99
|
+
const STUDIO_NAVIGATION_HELPERS_URL = new URL("./client/studio-navigation-helpers.js", import.meta.url);
|
|
93
100
|
const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
|
|
94
101
|
|
|
95
102
|
interface StudioServerState {
|
|
@@ -7259,6 +7266,17 @@ function respondText(res: ServerResponse, status: number, text: string): void {
|
|
|
7259
7266
|
res.end(text);
|
|
7260
7267
|
}
|
|
7261
7268
|
|
|
7269
|
+
function respondStudioPendingError(res: ServerResponse, status: number, text: string, allow?: string): void {
|
|
7270
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
7271
|
+
const headers = {
|
|
7272
|
+
...buildStudioPendingSecurityHeaders(nonce),
|
|
7273
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
7274
|
+
...(allow ? { Allow: allow } : {}),
|
|
7275
|
+
};
|
|
7276
|
+
res.writeHead(status, headers);
|
|
7277
|
+
res.end(text);
|
|
7278
|
+
}
|
|
7279
|
+
|
|
7262
7280
|
function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: string): void {
|
|
7263
7281
|
const method = (req.method ?? "GET").toUpperCase();
|
|
7264
7282
|
if (method !== "GET" && method !== "HEAD") {
|
|
@@ -7394,12 +7412,9 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
7394
7412
|
}
|
|
7395
7413
|
const document = buildStudioLocalResourcePreviewDocument(resource);
|
|
7396
7414
|
const docId = storeTransientStudioDocument(document);
|
|
7397
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
7398
|
-
const parsedUrl = new URL(url);
|
|
7399
7415
|
respondJson(res, 200, {
|
|
7400
7416
|
...basePayload,
|
|
7401
|
-
|
|
7402
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
7417
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
7403
7418
|
});
|
|
7404
7419
|
return;
|
|
7405
7420
|
}
|
|
@@ -7459,13 +7474,10 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
7459
7474
|
}
|
|
7460
7475
|
|
|
7461
7476
|
const docId = storeTransientStudioDocument(document);
|
|
7462
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
7463
|
-
const parsedUrl = new URL(url);
|
|
7464
7477
|
respondJson(res, 200, {
|
|
7465
7478
|
...basePayload,
|
|
7466
7479
|
converted,
|
|
7467
|
-
|
|
7468
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
7480
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
7469
7481
|
});
|
|
7470
7482
|
}
|
|
7471
7483
|
|
|
@@ -10078,8 +10090,7 @@ function readTransientStudioDocument(id: string): InitialStudioDocument | null {
|
|
|
10078
10090
|
return entry ? { ...entry.document } : null;
|
|
10079
10091
|
}
|
|
10080
10092
|
|
|
10081
|
-
function
|
|
10082
|
-
port: number,
|
|
10093
|
+
function buildStudioRelativeUrl(
|
|
10083
10094
|
token: string,
|
|
10084
10095
|
mode: StudioUiMode = "full",
|
|
10085
10096
|
doc?: InitialStudioDocument | null,
|
|
@@ -10095,7 +10106,18 @@ function buildStudioUrl(
|
|
|
10095
10106
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
10096
10107
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
10097
10108
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
10098
|
-
return
|
|
10109
|
+
return `/?${params.toString()}`;
|
|
10110
|
+
}
|
|
10111
|
+
|
|
10112
|
+
function buildStudioUrl(
|
|
10113
|
+
port: number,
|
|
10114
|
+
token: string,
|
|
10115
|
+
mode: StudioUiMode = "full",
|
|
10116
|
+
doc?: InitialStudioDocument | null,
|
|
10117
|
+
docId?: string,
|
|
10118
|
+
options?: { skipWorkspaceRestore?: boolean },
|
|
10119
|
+
): string {
|
|
10120
|
+
return `http://127.0.0.1:${port}${buildStudioRelativeUrl(token, mode, doc, docId, options)}`;
|
|
10099
10121
|
}
|
|
10100
10122
|
|
|
10101
10123
|
interface StudioLaunchFlags {
|
|
@@ -10509,6 +10531,7 @@ function buildStudioHtml(
|
|
|
10509
10531
|
const stylesheetHref = `/studio.css?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10510
10532
|
const annotationHelpersScriptHref = `/studio-annotation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10511
10533
|
const mermaidHelpersScriptHref = `/studio-mermaid-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10534
|
+
const navigationHelpersScriptHref = `/studio-navigation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10512
10535
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10513
10536
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
10514
10537
|
const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
|
|
@@ -10927,6 +10950,7 @@ ${cssVarsBlock}
|
|
|
10927
10950
|
</script>
|
|
10928
10951
|
<script src="${annotationHelpersScriptHref}"></script>
|
|
10929
10952
|
<script src="${mermaidHelpersScriptHref}"></script>
|
|
10953
|
+
<script src="${navigationHelpersScriptHref}"></script>
|
|
10930
10954
|
<script src="${clientScriptHref}"></script>
|
|
10931
10955
|
</body>
|
|
10932
10956
|
</html>`;
|
|
@@ -12852,13 +12876,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
12852
12876
|
resourceDir,
|
|
12853
12877
|
};
|
|
12854
12878
|
const docId = storeTransientStudioDocument(document);
|
|
12855
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
12856
|
-
const parsedUrl = new URL(url);
|
|
12857
12879
|
sendToClient(client, {
|
|
12858
12880
|
type: "editor_only_ready",
|
|
12859
12881
|
requestId: msg.requestId,
|
|
12860
|
-
|
|
12861
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
12882
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
12862
12883
|
message: hasContent
|
|
12863
12884
|
? "Editor tab is ready with a detached copy of the current editor text."
|
|
12864
12885
|
: "Blank editor tab is ready.",
|
|
@@ -14238,8 +14259,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
14238
14259
|
resourceDir: dirname(exportedPath),
|
|
14239
14260
|
};
|
|
14240
14261
|
const docId = storeTransientStudioDocument(document);
|
|
14241
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
14242
|
-
const parsedUrl = new URL(url);
|
|
14243
14262
|
respondJson(res, 200, {
|
|
14244
14263
|
ok: true,
|
|
14245
14264
|
filename,
|
|
@@ -14247,8 +14266,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14247
14266
|
writeError: writeResult.error,
|
|
14248
14267
|
warning: warning ?? null,
|
|
14249
14268
|
openedStudio: true,
|
|
14250
|
-
|
|
14251
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
14269
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
14252
14270
|
downloadUrl: `/export-pdf?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
14253
14271
|
});
|
|
14254
14272
|
return;
|
|
@@ -14385,8 +14403,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
14385
14403
|
draftId: exportedPath ? undefined : createStudioDraftId(),
|
|
14386
14404
|
};
|
|
14387
14405
|
const docId = storeTransientStudioDocument(document);
|
|
14388
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
14389
|
-
const parsedUrl = new URL(url);
|
|
14390
14406
|
respondJson(res, 200, {
|
|
14391
14407
|
ok: true,
|
|
14392
14408
|
filename,
|
|
@@ -14394,8 +14410,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14394
14410
|
writeError: writeResult.error,
|
|
14395
14411
|
warning: warning ?? null,
|
|
14396
14412
|
openedStudio: true,
|
|
14397
|
-
|
|
14398
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
14413
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
14399
14414
|
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
14400
14415
|
});
|
|
14401
14416
|
return;
|
|
@@ -14454,6 +14469,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
14454
14469
|
return;
|
|
14455
14470
|
}
|
|
14456
14471
|
|
|
14472
|
+
if (requestUrl.pathname === "/studio-open-pending") {
|
|
14473
|
+
const tokens = requestUrl.searchParams.getAll("token");
|
|
14474
|
+
if (tokens.length !== 1 || tokens[0] !== serverState.token) {
|
|
14475
|
+
respondStudioPendingError(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
14476
|
+
return;
|
|
14477
|
+
}
|
|
14478
|
+
|
|
14479
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
14480
|
+
if (method !== "GET") {
|
|
14481
|
+
respondStudioPendingError(res, 405, "Method not allowed. Use GET.", "GET");
|
|
14482
|
+
return;
|
|
14483
|
+
}
|
|
14484
|
+
|
|
14485
|
+
const allowedKeys = new Set(["token", "launchId", "kind"]);
|
|
14486
|
+
if (Array.from(requestUrl.searchParams.keys()).some((key) => !allowedKeys.has(key))) {
|
|
14487
|
+
respondStudioPendingError(res, 400, "Unsupported Studio pending-page parameter.");
|
|
14488
|
+
return;
|
|
14489
|
+
}
|
|
14490
|
+
const launchIds = requestUrl.searchParams.getAll("launchId");
|
|
14491
|
+
const kinds = requestUrl.searchParams.getAll("kind");
|
|
14492
|
+
const launchId = launchIds.length === 1 ? launchIds[0] : "";
|
|
14493
|
+
const kind = kinds.length === 1 ? normalizeStudioPendingKind(kinds[0]) : null;
|
|
14494
|
+
if (!isValidStudioLaunchId(launchId) || !kind) {
|
|
14495
|
+
respondStudioPendingError(res, 400, "Invalid Studio pending-page request.");
|
|
14496
|
+
return;
|
|
14497
|
+
}
|
|
14498
|
+
|
|
14499
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
14500
|
+
res.writeHead(200, buildStudioPendingSecurityHeaders(nonce));
|
|
14501
|
+
res.end(buildStudioPendingPage({ token: serverState.token, launchId, kind, nonce }));
|
|
14502
|
+
return;
|
|
14503
|
+
}
|
|
14504
|
+
|
|
14457
14505
|
if (requestUrl.pathname === "/studio.css") {
|
|
14458
14506
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14459
14507
|
if (token !== serverState.token) {
|
|
@@ -14483,7 +14531,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
14483
14531
|
return;
|
|
14484
14532
|
}
|
|
14485
14533
|
|
|
14486
|
-
if (
|
|
14534
|
+
if (
|
|
14535
|
+
requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14536
|
+
|| requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14537
|
+
|| requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14538
|
+
|| requestUrl.pathname === "/studio-client.js"
|
|
14539
|
+
) {
|
|
14487
14540
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14488
14541
|
if (token !== serverState.token) {
|
|
14489
14542
|
respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
@@ -14501,12 +14554,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
14501
14554
|
? STUDIO_ANNOTATION_HELPERS_URL
|
|
14502
14555
|
: requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14503
14556
|
? STUDIO_MERMAID_HELPERS_URL
|
|
14504
|
-
:
|
|
14557
|
+
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14558
|
+
? STUDIO_NAVIGATION_HELPERS_URL
|
|
14559
|
+
: STUDIO_CLIENT_URL;
|
|
14505
14560
|
const targetLabel = requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14506
14561
|
? "studio annotation helper script"
|
|
14507
14562
|
: requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14508
14563
|
? "studio Mermaid helper script"
|
|
14509
|
-
:
|
|
14564
|
+
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14565
|
+
? "studio navigation helper script"
|
|
14566
|
+
: "studio client script";
|
|
14510
14567
|
|
|
14511
14568
|
try {
|
|
14512
14569
|
const clientScript = readFileSync(targetUrl, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.41",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "node --test",
|
|
33
|
+
"test:browser": "node --test test/studio-tab-launcher-browser.test.js",
|
|
33
34
|
"typecheck": "tsc --noEmit"
|
|
34
35
|
},
|
|
35
36
|
"pi": {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const STUDIO_PENDING_KINDS = Object.freeze(["document", "preview", "export"]);
|
|
2
|
+
const STUDIO_LAUNCH_ID_PATTERN = /^[a-zA-Z0-9_-]{20,128}$/;
|
|
3
|
+
const STUDIO_CSP_NONCE_PATTERN = /^[a-zA-Z0-9_-]{16,128}$/;
|
|
4
|
+
|
|
5
|
+
function escapeHtmlAttribute(value) {
|
|
6
|
+
return String(value ?? "")
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeStudioPendingKind(value) {
|
|
14
|
+
return STUDIO_PENDING_KINDS.includes(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isValidStudioLaunchId(value) {
|
|
18
|
+
return typeof value === "string" && STUDIO_LAUNCH_ID_PATTERN.test(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildStudioPendingSecurityHeaders(nonce) {
|
|
22
|
+
if (typeof nonce !== "string" || !STUDIO_CSP_NONCE_PATTERN.test(nonce)) {
|
|
23
|
+
throw new Error("Invalid Studio pending-page CSP nonce.");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
27
|
+
"Cache-Control": "no-store",
|
|
28
|
+
"X-Content-Type-Options": "nosniff",
|
|
29
|
+
"Referrer-Policy": "no-referrer",
|
|
30
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
31
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
32
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
|
33
|
+
"Content-Security-Policy": [
|
|
34
|
+
"default-src 'none'",
|
|
35
|
+
`script-src 'nonce-${nonce}'`,
|
|
36
|
+
`style-src 'nonce-${nonce}'`,
|
|
37
|
+
"base-uri 'none'",
|
|
38
|
+
"form-action 'none'",
|
|
39
|
+
"object-src 'none'",
|
|
40
|
+
"frame-ancestors 'none'",
|
|
41
|
+
].join("; "),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildStudioPendingPage(options) {
|
|
46
|
+
const config = options && typeof options === "object" ? options : {};
|
|
47
|
+
const token = typeof config.token === "string" ? config.token : "";
|
|
48
|
+
const launchId = typeof config.launchId === "string" ? config.launchId : "";
|
|
49
|
+
const kind = normalizeStudioPendingKind(config.kind);
|
|
50
|
+
const nonce = typeof config.nonce === "string" ? config.nonce : "";
|
|
51
|
+
if (!token) throw new Error("Studio pending page requires a token.");
|
|
52
|
+
if (!isValidStudioLaunchId(launchId)) throw new Error("Invalid Studio launch ID.");
|
|
53
|
+
if (!kind) throw new Error("Invalid Studio pending-page kind.");
|
|
54
|
+
buildStudioPendingSecurityHeaders(nonce);
|
|
55
|
+
|
|
56
|
+
const kindLabel = kind === "document" ? "document" : (kind === "export" ? "export" : "preview");
|
|
57
|
+
const helperQuery = new URLSearchParams({ token }).toString();
|
|
58
|
+
return `<!doctype html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8" />
|
|
62
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
63
|
+
<title>Preparing Studio tab…</title>
|
|
64
|
+
<style nonce="${escapeHtmlAttribute(nonce)}">
|
|
65
|
+
:root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
66
|
+
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: Canvas; color: CanvasText; }
|
|
67
|
+
main { width: min(34rem, calc(100vw - 3rem)); padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 18%, transparent); border-radius: 12px; }
|
|
68
|
+
h1 { margin: 0 0 0.65rem; font-size: 1.15rem; }
|
|
69
|
+
p { margin: 0; line-height: 1.5; opacity: 0.8; overflow-wrap: anywhere; }
|
|
70
|
+
button { margin-top: 1.25rem; padding: 0.45rem 0.8rem; font: inherit; }
|
|
71
|
+
[hidden] { display: none !important; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body data-studio-pending-launch="1" data-launch-id="${escapeHtmlAttribute(launchId)}" data-launch-kind="${escapeHtmlAttribute(kind)}" data-studio-token="${escapeHtmlAttribute(token)}">
|
|
75
|
+
<main aria-live="polite" aria-atomic="true">
|
|
76
|
+
<h1 id="pendingTitle">Preparing Studio ${kindLabel}…</h1>
|
|
77
|
+
<p id="pendingDetail">Waiting for the originating Studio page.</p>
|
|
78
|
+
<button id="pendingCloseBtn" type="button" hidden>Close tab</button>
|
|
79
|
+
</main>
|
|
80
|
+
<noscript>This Studio tab requires JavaScript. You can close it and return to the originating Studio page.</noscript>
|
|
81
|
+
<script nonce="${escapeHtmlAttribute(nonce)}" src="/studio-navigation-helpers.js?${escapeHtmlAttribute(helperQuery)}"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { STUDIO_PENDING_KINDS };
|