@kubohiroya/turbowarp-plus-editor-git 0.18.0 → 0.19.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.
Files changed (42) hide show
  1. package/README.ja.md +10 -2
  2. package/README.md +10 -2
  3. package/dist/branch-button.d.ts +15 -0
  4. package/dist/change-marks.d.ts +8 -0
  5. package/dist/connection-settings.d.ts +3 -1
  6. package/dist/github-pull-requests.d.ts +2 -2
  7. package/dist/github-review-api.d.ts +112 -0
  8. package/dist/index.d.ts +19 -0
  9. package/dist/index.js +377 -268
  10. package/dist/messages.d.ts +101 -0
  11. package/dist/{project-asset-paths-BzwVf9HE.js → project-asset-paths-DNaoWs3E.js} +1 -1
  12. package/dist/{project-comparison-BXOQcZ73.js → project-comparison-B_3QZ6M-.js} +1414 -33
  13. package/dist/project-source-diff.d.ts +5 -0
  14. package/dist/project-source-diff.js +4 -4
  15. package/dist/project-source-review.d.ts +6 -1
  16. package/dist/pull-request-panel.d.ts +2 -0
  17. package/dist/remote-operations.d.ts +1 -1
  18. package/dist/{repository-ByKRBftJ.js → repository-DXRZEtYk.js} +2 -3
  19. package/dist/repository.js +1 -1
  20. package/dist/review-composer.d.ts +23 -0
  21. package/dist/review-submit.d.ts +144 -0
  22. package/dist/{smart-http-BmHEkE7K.js → smart-http-eARzlXHO.js} +101 -2
  23. package/dist/smart-http.d.ts +2 -0
  24. package/dist/smart-http.js +1 -1
  25. package/dist/style.css +23 -0
  26. package/package.json +1 -1
  27. package/src/branch-button.tsx +79 -6
  28. package/src/change-marks.ts +22 -0
  29. package/src/connection-settings.tsx +9 -2
  30. package/src/github-review-api.ts +298 -0
  31. package/src/index.tsx +153 -15
  32. package/src/messages.ts +76 -2
  33. package/src/project-source-diff.css +10 -0
  34. package/src/project-source-diff.tsx +6 -0
  35. package/src/project-source-review.tsx +33 -5
  36. package/src/pull-request-panel.tsx +27 -6
  37. package/src/remote-operations.ts +1 -1
  38. package/src/review-composer.tsx +166 -0
  39. package/src/review-submit.ts +519 -0
  40. package/src/smart-http.ts +2 -2
  41. package/src/style.css +13 -0
  42. package/dist/objectSpread2-Dkd68nJH.js +0 -101
@@ -1,5 +1,5 @@
1
- import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
2
- import { a as compareProjectBlocks, c as isRecord, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
1
+ import { a as remoteUrl, l as _objectSpread2, r as checkConnection, u as init_objectSpread2 } from "./smart-http-eARzlXHO.js";
2
+ import { a as compareProjectBlocks, c as isRecord$2, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-DNaoWs3E.js";
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { render, unmountComponentAtNode } from "react-dom";
5
5
  // src/close-button.tsx
@@ -41,7 +41,7 @@ function currentCostume(target) {
41
41
  const { costumes, currentCostume: index } = target;
42
42
  if (!Array.isArray(costumes)) return null;
43
43
  const costume = costumes[typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : 0];
44
- if (!isRecord(costume)) return null;
44
+ if (!isRecord$2(costume)) return null;
45
45
  const md5ext = typeof costume.md5ext === "string" ? costume.md5ext : typeof costume.assetId === "string" && typeof costume.dataFormat === "string" ? costume.assetId + "." + costume.dataFormat : "";
46
46
  const match = /^[0-9A-Za-z_-]{1,128}\.([0-9A-Za-z]{1,8})$/.exec(md5ext);
47
47
  const type = match ? imageTypes[match[1].toLowerCase()] : void 0;
@@ -243,7 +243,7 @@ function scriptRoots(target) {
243
243
  while (!roots.has(current) && !chain.has(current)) {
244
244
  chain.add(current);
245
245
  const block = target.blocks[current];
246
- if (!isRecord(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) break;
246
+ if (!isRecord$2(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) break;
247
247
  current = block.parent;
248
248
  }
249
249
  const root = (_roots$get = roots.get(current)) !== null && _roots$get !== void 0 ? _roots$get : current;
@@ -351,12 +351,12 @@ function scriptKind(group, changes) {
351
351
  if (changes.every(isLayoutOnly)) return "reordered";
352
352
  return "updated";
353
353
  }
354
- var opcodeOf = (block) => isRecord(block) && typeof block.opcode === "string" ? block.opcode : void 0;
355
- var isShadow = (block) => !isRecord(block) || block.shadow === true;
354
+ var opcodeOf = (block) => isRecord$2(block) && typeof block.opcode === "string" ? block.opcode : void 0;
355
+ var isShadow = (block) => !isRecord$2(block) || block.shadow === true;
356
356
  var isHat = (opcode) => opcode !== void 0 && (opcode === "control_start_as_clone" || opcode === "procedures_definition" || /^[a-z0-9]+_when/i.test(opcode));
357
357
  var substackInputs = ["SUBSTACK", "SUBSTACK2"];
358
358
  function inputBlock(block, name) {
359
- if (!isRecord(block) || !isRecord(block.inputs)) return void 0;
359
+ if (!isRecord$2(block) || !isRecord$2(block.inputs)) return void 0;
360
360
  const input = block.inputs[name];
361
361
  return Array.isArray(input) && typeof input[1] === "string" ? input[1] : void 0;
362
362
  }
@@ -401,7 +401,7 @@ function summarize(group, changes, before, after) {
401
401
  visited.add(id);
402
402
  if (before && Object.hasOwn(before.blocks, id)) return true;
403
403
  const next = after.blocks[id];
404
- id = isRecord(next) && typeof next.next === "string" ? next.next : void 0;
404
+ id = isRecord$2(next) && typeof next.next === "string" ? next.next : void 0;
405
405
  }
406
406
  return false;
407
407
  })) {
@@ -542,7 +542,7 @@ function statusOf(before, after) {
542
542
  }
543
543
  function entriesOf(target, kind) {
544
544
  const list = target === null || target === void 0 ? void 0 : target[kind];
545
- return Array.isArray(list) ? list.filter(isRecord) : [];
545
+ return Array.isArray(list) ? list.filter(isRecord$2) : [];
546
546
  }
547
547
  function nameOf(entry) {
548
548
  return typeof entry.name === "string" ? entry.name : "";
@@ -787,6 +787,1356 @@ var legend = (locale) => locale.startsWith("ja") ? [
787
787
  ["updated", "Purple outline: block with changed contents"],
788
788
  ["part", "Orange frame: changed value"]
789
789
  ];
790
+ // src/github-review-api.ts
791
+ init_objectSpread2();
792
+ var reviewEvents = [
793
+ "COMMENT",
794
+ "APPROVE",
795
+ "REQUEST_CHANGES"
796
+ ];
797
+ var proxyCodes = {
798
+ 400: ["BAD_REQUEST"],
799
+ 401: [
800
+ "SESSION_REQUIRED",
801
+ "SESSION_EXPIRED",
802
+ "REAUTH_REQUIRED"
803
+ ],
804
+ 403: ["FORBIDDEN"],
805
+ 404: ["GITHUB_API_UNAVAILABLE", "PULL_REQUEST_NOT_FOUND"],
806
+ 409: [
807
+ "REVIEW_STEP_CONFLICT",
808
+ "REVIEW_DUPLICATE",
809
+ "PENDING_REVIEW_EXISTS",
810
+ "REDIRECT_REJECTED"
811
+ ],
812
+ 411: ["LENGTH_REQUIRED"],
813
+ 413: ["PAYLOAD_TOO_LARGE"],
814
+ 422: ["REVIEW_REJECTED", "REVIEW_THREAD_REJECTED"],
815
+ 429: ["RATE_LIMITED"],
816
+ 500: ["GITHUB_API_MISCONFIGURED"],
817
+ 502: ["UPSTREAM_FAILURE"],
818
+ 504: ["UPSTREAM_TIMEOUT"]
819
+ };
820
+ var unknownCodes = [
821
+ "REVIEW_DUPLICATE",
822
+ "UPSTREAM_FAILURE",
823
+ "UPSTREAM_TIMEOUT",
824
+ "NETWORK_ERROR",
825
+ "INVALID_RESPONSE"
826
+ ];
827
+ var GitHubReviewError = class extends Error {
828
+ constructor(code, options = {}) {
829
+ super(code);
830
+ this.code = code;
831
+ this.options = options;
832
+ this.name = "GitHubReviewError";
833
+ }
834
+ get unknown() {
835
+ return this.options.unknown === true;
836
+ }
837
+ get retryAfter() {
838
+ return this.options.retryAfter;
839
+ }
840
+ };
841
+ var isSha$2 = (value) => typeof value === "string" && /^[0-9a-f]{40}$/u.test(value);
842
+ var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
843
+ var isCount = (value, max) => Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= max;
844
+ var optionalString = (value) => value === null || typeof value === "string";
845
+ var submitIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
846
+ var requestIdPattern = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
847
+ /** A thread path the proxy accepts: a relative path whose last element is exactly project.source.json. */
848
+ function isReviewThreadPath(path) {
849
+ const parts = path.split("/");
850
+ return parts[parts.length - 1] === "project.source.json" && new TextEncoder().encode(path).byteLength <= 1024 && parts.every((part) => part && part !== "." && part !== "..") && !/[\\\u0000-\u001f\u007f]/u.test(path);
851
+ }
852
+ function newReviewSubmitId() {
853
+ const id = crypto.randomUUID();
854
+ if (!submitIdPattern.test(id)) throw new GitHubReviewError("INVALID_RESPONSE");
855
+ return id;
856
+ }
857
+ function readContext(value, withSubmit) {
858
+ if (!isRecord$1(value) || !isSha$2(value.headRefOid) || typeof value.viewer !== "string" || !Array.isArray(value.reviews)) throw new GitHubReviewError("INVALID_RESPONSE");
859
+ let pendingReview = null;
860
+ if (value.pendingReview !== null) {
861
+ const pending = value.pendingReview;
862
+ if (!isRecord$1(pending) || typeof pending.ours !== "boolean" || !(pending.commitOid === null || isSha$2(pending.commitOid)) || typeof pending.createdAt !== "string") throw new GitHubReviewError("INVALID_RESPONSE");
863
+ pendingReview = {
864
+ ours: pending.ours,
865
+ commitOid: pending.commitOid,
866
+ createdAt: pending.createdAt
867
+ };
868
+ }
869
+ const reviews = value.reviews.map((review) => {
870
+ if (!isRecord$1(review) || typeof review.state !== "string" || !(review.commitOid === null || isSha$2(review.commitOid)) || !optionalString(review.submittedAt) || typeof review.ours !== "boolean") throw new GitHubReviewError("INVALID_RESPONSE");
871
+ return {
872
+ state: review.state,
873
+ commitOid: review.commitOid,
874
+ submittedAt: review.submittedAt,
875
+ ours: review.ours
876
+ };
877
+ });
878
+ const context = {
879
+ headRefOid: value.headRefOid,
880
+ viewer: value.viewer,
881
+ pendingReview,
882
+ reviews
883
+ };
884
+ if (withSubmit) {
885
+ const submit = value.submit;
886
+ if (!isRecord$1(submit) || typeof submit.state !== "string" || !isCount(submit.threads, 50)) throw new GitHubReviewError("INVALID_RESPONSE");
887
+ context.submit = {
888
+ state: submit.state,
889
+ threads: submit.threads
890
+ };
891
+ }
892
+ return context;
893
+ }
894
+ async function readBounded(response, limit) {
895
+ if (!response.body) return "";
896
+ const reader = response.body.getReader();
897
+ const decoder = new TextDecoder("utf-8", { fatal: true });
898
+ let text = "", size = 0;
899
+ try {
900
+ for (;;) {
901
+ const { done, value } = await reader.read();
902
+ if (done) return text + decoder.decode();
903
+ size += value.byteLength;
904
+ if (size > limit) throw new GitHubReviewError("INVALID_RESPONSE");
905
+ text += decoder.decode(value, { stream: true });
906
+ }
907
+ } finally {
908
+ reader.cancel().catch(() => {});
909
+ }
910
+ }
911
+ function createGitHubReviewApi({ remote, getSession, fetch: send = (...args) => fetch(...args) }) {
912
+ let path;
913
+ try {
914
+ path = "/v1/github" + remoteUrl(remote).slice(18).replace(/\.git$/u, "");
915
+ } catch (_unused) {
916
+ throw new GitHubReviewError("INVALID_REMOTE");
917
+ }
918
+ /** What the proxy answered about each route group: `false` after `404 GITHUB_API_UNAVAILABLE`. */
919
+ const available = {};
920
+ let account = null;
921
+ async function session(review) {
922
+ var _value$account;
923
+ let value;
924
+ try {
925
+ value = await getSession(remote, false, review);
926
+ } catch (_unused2) {
927
+ throw new GitHubReviewError("SESSION_UNAVAILABLE");
928
+ }
929
+ if (!value) throw new GitHubReviewError("SESSION_REQUIRED");
930
+ if (!Number.isFinite(value.expiresAt) || value.expiresAt <= Date.now()) throw new GitHubReviewError("SESSION_EXPIRED");
931
+ try {
932
+ checkConnection(value.connection);
933
+ } catch (_unused3) {
934
+ throw new GitHubReviewError("INVALID_CONNECTION");
935
+ }
936
+ if (typeof value.bearer !== "string" || !/^[A-Za-z0-9._~+\/-]+=*$/u.test(value.bearer)) throw new GitHubReviewError("INVALID_SESSION");
937
+ account = typeof ((_value$account = value.account) === null || _value$account === void 0 ? void 0 : _value$account.login) === "string" ? value.account.login : null;
938
+ return value;
939
+ }
940
+ async function call(route, { method = "GET", body, submitId, review = false, mutation = false, limit = 262144, group, signal }) {
941
+ var _response$headers$get;
942
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) throw new GitHubReviewError("ABORTED");
943
+ const current = await session(review);
944
+ const headers = {
945
+ Authorization: "Bearer " + current.bearer,
946
+ Accept: "application/json"
947
+ };
948
+ if (body !== void 0) headers["Content-Type"] = "application/json";
949
+ if (submitId !== void 0) {
950
+ if (!submitIdPattern.test(submitId)) throw new GitHubReviewError("BAD_REQUEST");
951
+ headers["Review-Submit-Id"] = submitId;
952
+ }
953
+ let response;
954
+ try {
955
+ response = await send(current.connection.proxyOrigin + path + route, {
956
+ method,
957
+ headers,
958
+ body: body === void 0 ? void 0 : JSON.stringify(body),
959
+ signal,
960
+ credentials: "omit",
961
+ redirect: "error",
962
+ cache: "no-store",
963
+ referrerPolicy: "no-referrer"
964
+ });
965
+ } catch (_unused4) {
966
+ if (!mutation && (signal === null || signal === void 0 ? void 0 : signal.aborted)) throw new GitHubReviewError("ABORTED");
967
+ throw new GitHubReviewError("NETWORK_ERROR", { unknown: mutation });
968
+ }
969
+ const json = ((_response$headers$get = response.headers.get("content-type")) === null || _response$headers$get === void 0 ? void 0 : _response$headers$get.split(";")[0].trim()) === "application/json";
970
+ if (response.status !== 200) {
971
+ var _allowed$;
972
+ const allowed = proxyCodes[response.status];
973
+ let code = (_allowed$ = allowed === null || allowed === void 0 ? void 0 : allowed[0]) !== null && _allowed$ !== void 0 ? _allowed$ : "INVALID_RESPONSE";
974
+ let requestId, retryAfter;
975
+ try {
976
+ const parsed = json ? JSON.parse(await readBounded(response, 4096)) : null;
977
+ if (isRecord$1(parsed)) {
978
+ if (allowed === null || allowed === void 0 ? void 0 : allowed.includes(parsed.code)) code = parsed.code;
979
+ if (typeof parsed.requestId === "string" && requestIdPattern.test(parsed.requestId)) requestId = parsed.requestId;
980
+ if (Number.isSafeInteger(parsed.retryAfter) && Number(parsed.retryAfter) >= 0) retryAfter = Number(parsed.retryAfter);
981
+ }
982
+ } catch (_unused5) {}
983
+ if (code === "GITHUB_API_UNAVAILABLE" && group !== "read") available[group] = false;
984
+ if (code === "GITHUB_API_UNAVAILABLE" && group === "read") available.threads = false;
985
+ const unknown = mutation && (unknownCodes.includes(code) || !allowed);
986
+ throw new GitHubReviewError(unknown && !allowed ? "UPSTREAM_FAILURE" : code, {
987
+ status: response.status,
988
+ requestId,
989
+ retryAfter,
990
+ unknown
991
+ });
992
+ }
993
+ if (group !== "read") available[group] = true;
994
+ try {
995
+ if (!json) throw new Error();
996
+ return JSON.parse(await readBounded(response, limit));
997
+ } catch (_unused6) {
998
+ throw new GitHubReviewError("INVALID_RESPONSE", { unknown: mutation });
999
+ }
1000
+ }
1001
+ const checkNumber = (number) => {
1002
+ if (!Number.isSafeInteger(number) || number < 1 || number > 2147483647) throw new GitHubReviewError("BAD_REQUEST");
1003
+ };
1004
+ const graphql = (operation, number, variables, submitId, signal, mutation = true) => {
1005
+ checkNumber(number);
1006
+ return call("/graphql/" + operation, {
1007
+ method: "POST",
1008
+ body: _objectSpread2({ number }, variables),
1009
+ submitId,
1010
+ review: mutation,
1011
+ mutation,
1012
+ group: mutation ? "threads" : "read",
1013
+ signal
1014
+ });
1015
+ };
1016
+ const shape = (check, value, mutation = true) => {
1017
+ if (!check) throw new GitHubReviewError("INVALID_RESPONSE", { unknown: mutation });
1018
+ return value;
1019
+ };
1020
+ return {
1021
+ available,
1022
+ /** The GitHub login of the last session this client used. */
1023
+ account: () => account,
1024
+ /** Reads the viewer's pending and recent reviews; with `submitId`, resolves that submission's unknown outcome. */
1025
+ async context(number, submitId, signal) {
1026
+ const context = readContext(await graphql("pullRequestReviewContext", number, {}, submitId, signal, false), submitId !== void 0);
1027
+ available.threads = true;
1028
+ return context;
1029
+ },
1030
+ /** Step 1: a pending review on `commitOid`. */
1031
+ async start(number, submitId, commitOid, signal) {
1032
+ if (!isSha$2(commitOid)) throw new GitHubReviewError("BAD_REQUEST");
1033
+ const value = await graphql("addPullRequestReview", number, { commitOid }, submitId, signal);
1034
+ shape(isRecord$1(value) && value.state === "PENDING" && value.commitOid === commitOid, value);
1035
+ },
1036
+ /** Step 2: one file-level thread on a project.source.json; resolves the number of threads the proxy has accepted. */
1037
+ async thread(number, submitId, threadIndex, threadPath, body, signal) {
1038
+ if (!isCount(threadIndex, 49) || !isReviewThreadPath(threadPath) || !body || body.length > 65536) throw new GitHubReviewError("BAD_REQUEST");
1039
+ const value = await graphql("addPullRequestReviewThread", number, {
1040
+ threadIndex,
1041
+ path: threadPath,
1042
+ body
1043
+ }, submitId, signal);
1044
+ return shape(isRecord$1(value) && value.threads === threadIndex + 1, threadIndex + 1);
1045
+ },
1046
+ /** Step 3: publishes the pending review. */
1047
+ async submit(number, submitId, threadCount, event, body, signal) {
1048
+ if (!isCount(threadCount, 50) || !reviewEvents.includes(event) || body.length > 65536) throw new GitHubReviewError("BAD_REQUEST");
1049
+ const value = await graphql("submitPullRequestReview", number, {
1050
+ threadCount,
1051
+ event,
1052
+ body
1053
+ }, submitId, signal);
1054
+ const review = isRecord$1(value) ? value : {};
1055
+ return shape(typeof review.state === "string" && review.state !== "PENDING" && isSha$2(review.commitOid) && typeof review.submittedAt === "string" && typeof review.url === "string", review);
1056
+ },
1057
+ /** Removes the pending review of the submission. */
1058
+ async discard(number, submitId, signal) {
1059
+ const value = await graphql("deletePullRequestReview", number, {}, submitId, signal);
1060
+ shape(isRecord$1(value) && value.deleted === true, value);
1061
+ },
1062
+ /** The review-body route: one page of the pull request's reviews, oldest first. */
1063
+ async reviews(number, page, signal) {
1064
+ checkNumber(number);
1065
+ if (!Number.isSafeInteger(page) || page < 1 || page > 10) throw new GitHubReviewError("BAD_REQUEST");
1066
+ const value = await call(`/pulls/${number}/reviews?page=${page}`, {
1067
+ group: "body",
1068
+ limit: 4194304,
1069
+ signal
1070
+ });
1071
+ if (!Array.isArray(value)) throw new GitHubReviewError("INVALID_RESPONSE");
1072
+ return value.map((review) => {
1073
+ if (!isRecord$1(review) || !Number.isSafeInteger(review.id) || typeof review.state !== "string" || !(review.commit_id === null || isSha$2(review.commit_id)) || !optionalString(review.body)) throw new GitHubReviewError("INVALID_RESPONSE");
1074
+ const login = isRecord$1(review.user) && typeof review.user.login === "string" ? review.user.login : null;
1075
+ return {
1076
+ id: Number(review.id),
1077
+ login,
1078
+ commitId: review.commit_id,
1079
+ state: review.state,
1080
+ body: typeof review.body === "string" ? review.body : ""
1081
+ };
1082
+ });
1083
+ },
1084
+ /** The review-body route: creates and publishes a review with a body only. */
1085
+ async create(number, submitId, commitOid, event, body, signal) {
1086
+ checkNumber(number);
1087
+ if (!isSha$2(commitOid) || !reviewEvents.includes(event) || body.length > 65536 || !body && event !== "APPROVE") throw new GitHubReviewError("BAD_REQUEST");
1088
+ const value = await call(`/pulls/${number}/reviews`, {
1089
+ method: "POST",
1090
+ body: {
1091
+ commit_id: commitOid,
1092
+ event,
1093
+ body
1094
+ },
1095
+ submitId,
1096
+ review: true,
1097
+ mutation: true,
1098
+ group: "body",
1099
+ limit: 1048576,
1100
+ signal
1101
+ });
1102
+ return shape(isRecord$1(value) && typeof value.state === "string" && typeof value.html_url === "string", { url: value.html_url });
1103
+ }
1104
+ };
1105
+ }
1106
+ // src/messages.ts
1107
+ var messages = {
1108
+ en: {
1109
+ pullRequests: "Pull requests",
1110
+ localChanges: "Local changes",
1111
+ title: "Local Git",
1112
+ close: "Close",
1113
+ choose: "Choose Repository…",
1114
+ refresh: "Refresh",
1115
+ empty: "Choose an existing Git repository to inspect its files and history.",
1116
+ repository: "Repository",
1117
+ project: "Project folder",
1118
+ none: "No project selected",
1119
+ root: "Repository root",
1120
+ open: "Open This Project",
1121
+ reload: "Reload Project",
1122
+ up: "Parent folder",
1123
+ browse: "Choose a project inside this repository",
1124
+ replace: "Replace the open project and discard its unsaved changes?",
1125
+ dirty: "The Editor has unsaved changes.",
1126
+ saved: "The Editor has no unsaved changes.",
1127
+ staged: "Changes in the next record",
1128
+ unstaged: "Saved, not yet recorded",
1129
+ untracked: "New files never recorded",
1130
+ commit: "Changes in this record",
1131
+ history: "Records (commits)",
1132
+ worktree: "Saved files",
1133
+ clean: "No saved changes in the selected folder.",
1134
+ branches: "Local branches",
1135
+ unborn: "No records yet",
1136
+ detached: "Detached HEAD",
1137
+ select: "Select a file to view its diff.",
1138
+ binary: "Binary asset",
1139
+ large: "Text preview exceeds its display limit.",
1140
+ bytes: "bytes",
1141
+ mode: "Git file mode",
1142
+ permissions: "Working-tree executable permission changes are unavailable in the browser.",
1143
+ unchanged: "The text is unchanged.",
1144
+ loading: "Reading repository…",
1145
+ invalid: "Choose a repository with a .git directory, and a project inside that repository.",
1146
+ changed: "Files or Git state changed while reading. Refresh to read the current state.",
1147
+ outdated: "The repository changed outside the Editor. Refresh to read the current state.",
1148
+ unsupported: "This repository contains an entry the browser cannot represent.",
1149
+ commitMessage: "Write a message for this record.",
1150
+ switchUnsafe: "Stop the project, and save or record (or undo) every change, before switching branches.",
1151
+ branchExists: "A branch with this name already exists.",
1152
+ branchConflict: "This name clashes with an existing branch: Git cannot keep both a branch and another branch under its name followed by /, such as pr and pr/7. Rename or delete the other branch with Git, or choose another name.",
1153
+ pullRequestBranch: "The branch pr/<number> already exists with other records, so this version was not opened. The branch is left unchanged. Switch to it in Local changes, or rename or delete it with Git, then open the pull request here again.",
1154
+ openedPullRequest: "Opened pull request #{number} on the local branch {branch}. To go back, switch to {previous} in Local changes.",
1155
+ openedPullRequestDetached: "Opened pull request #{number} on the local branch {branch}. To go back, switch to another branch in Local changes.",
1156
+ locked: "Another Git tool is working on this repository (a lock file, or a merge, cherry-pick, revert or rebase in progress). Try again after it finishes.",
1157
+ branchName: "Git cannot use this name for a branch.",
1158
+ pullUnsafe: "Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).",
1159
+ diverged: "This branch and GitHub's have both gained records, so they cannot be combined here (pull only fast-forwards). Your branch and files are unchanged.",
1160
+ session: "Reconnect GitHub for this repository and requested permission.",
1161
+ pushUnknown: "It is unknown whether GitHub received the records. Check GitHub (fetch) before sending again.",
1162
+ pushRefresh: "GitHub received the records. Check GitHub (fetch) to update the status.",
1163
+ rollbackFailed: "Some local changes could not be restored. Check the files and the Git details.",
1164
+ remoteFailed: "Could not complete the GitHub operation. Check the connection and try again.",
1165
+ remoteDone: "The GitHub operation finished.",
1166
+ stage: "Add to Next Record",
1167
+ unstage: "Remove from Next Record",
1168
+ stageAll: "Add All Changes to Next Record",
1169
+ commitLabel: "Message for this record",
1170
+ commitButton: "Record (Commit)",
1171
+ committed: "Recorded the changes (commit).",
1172
+ commitAuthor: "Recorded as",
1173
+ saveFirst: "Save the project before adding its changes to the next record.",
1174
+ author: "Set user.name and user.email for this repository in Git before recording. The Editor does not change Git settings.",
1175
+ nothingStaged: "Add changes to the next record first.",
1176
+ switchTo: "Switch",
1177
+ currentBranch: "current",
1178
+ createBranch: "Create Branch",
1179
+ newBranch: "New branch name",
1180
+ switchAfterCreate: "Switch to it",
1181
+ createFromRemote: "Create and Switch",
1182
+ remoteBranches: "Branches on GitHub (origin)",
1183
+ switched: "Switched branches. Use Reload Project to open the files of this branch.",
1184
+ branchCreated: "Created the branch.",
1185
+ projectMissing: "The open project folder is not on this branch. Switch anyway?",
1186
+ pulledProject: "Got the changes from GitHub. Use Reload Project to open the updated files.",
1187
+ confirmPush: "Send the records on this branch to GitHub (push)?",
1188
+ details: "Git details",
1189
+ branch: "Branch",
1190
+ head: "HEAD",
1191
+ groups: "Git groups",
1192
+ added: "Added",
1193
+ modified: "Changed",
1194
+ deleted: "Deleted",
1195
+ gitStaged: "Staged",
1196
+ gitUnstaged: "Unstaged",
1197
+ gitUntracked: "Untracked",
1198
+ upstreamSame: "Same as GitHub",
1199
+ upstreamAhead: "Ahead of GitHub",
1200
+ upstreamBehind: "Behind GitHub",
1201
+ upstreamDiverged: "Diverged from GitHub",
1202
+ upstreamUnknown: "Not checked with GitHub yet",
1203
+ records: "records",
1204
+ checkedAt: "as of the last check",
1205
+ failed: "Could not read the repository. Check folder access and refresh.",
1206
+ historyMore: "Show more",
1207
+ historyLoading: "Reading records…",
1208
+ historyFailed: "Could not read the records.",
1209
+ historyActions: "Actions",
1210
+ historyCreateHere: "Create branch here…",
1211
+ historySwitch: "Switch to {name}",
1212
+ historyCreateFromRemote: "Create and switch to {name}",
1213
+ historyCopy: "Copy commit ID",
1214
+ historyCopied: "Copied the commit ID.",
1215
+ historyCopyFailed: "Could not copy the commit ID.",
1216
+ historyRefs: "Branches",
1217
+ cancel: "Cancel",
1218
+ branchMenu: "Branches",
1219
+ openPanel: "Open Local Git",
1220
+ newBranchItem: "New Branch…",
1221
+ newBranchTitle: "New Branch",
1222
+ switchedShort: "Switched branches.",
1223
+ historyViewEmpty: "Choose a Git repository to show its history here."
1224
+ },
1225
+ ja: {
1226
+ pullRequests: "プルリクエスト",
1227
+ localChanges: "ローカルの変更",
1228
+ title: "ローカル Git",
1229
+ close: "閉じる",
1230
+ choose: "リポジトリを選択…",
1231
+ refresh: "再取得",
1232
+ empty: "既存の Git リポジトリを選び、ファイルの状態と履歴を確認できます。",
1233
+ repository: "リポジトリ",
1234
+ project: "作品フォルダ",
1235
+ none: "作品は未選択",
1236
+ root: "リポジトリルート",
1237
+ open: "この作品を開く",
1238
+ reload: "作品を再読込",
1239
+ up: "上のフォルダ",
1240
+ browse: "リポジトリ内の作品を選択",
1241
+ replace: "保存していない編集を破棄して、別の作品を開きますか?",
1242
+ dirty: "Editor に保存していない編集があります。",
1243
+ saved: "Editor に保存していない編集はありません。",
1244
+ staged: "次の記録に入る変更",
1245
+ unstaged: "保存したが、まだ記録していない変更",
1246
+ untracked: "まだ記録したことのない新しいファイル",
1247
+ commit: "この記録の変更",
1248
+ history: "記録(コミット)の履歴",
1249
+ worktree: "保存したファイル",
1250
+ clean: "対象フォルダに、保存した変更はありません。",
1251
+ branches: "ローカルブランチ",
1252
+ unborn: "まだ記録がありません",
1253
+ detached: "ブランチ未選択の HEAD",
1254
+ select: "ファイルを選ぶと差分を表示します。",
1255
+ binary: "バイナリアセット",
1256
+ large: "テキスト差分の表示上限を超えています。",
1257
+ bytes: "バイト",
1258
+ mode: "Git のファイルモード",
1259
+ permissions: "作業ツリーの実行権限の変更はブラウザーから確認できません。",
1260
+ unchanged: "テキストに変更はありません。",
1261
+ loading: "リポジトリを読込中…",
1262
+ invalid: ".git ディレクトリを持つリポジトリと、その配下の作品を選んでください。",
1263
+ changed: "読込中にファイルまたは Git の状態が変わりました。再取得してください。",
1264
+ outdated: "Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。",
1265
+ unsupported: "ブラウザーで扱えない種類の Git エントリーがあります。",
1266
+ commitMessage: "この記録の説明を書いてください。",
1267
+ switchUnsafe: "ブランチを切り替える前に、実行を止め、すべての変更を保存して記録(または取り消し)してください。",
1268
+ branchExists: "同じ名前のブランチがすでにあります。",
1269
+ branchConflict: "この名前は既存のブランチとぶつかります。Git では、pr と pr/7 のように、あるブランチと、その名前に / を続けたブランチを両方は持てません。Git でもう一方の名前を変えるか消すか、別の名前にしてください。",
1270
+ pullRequestBranch: "ブランチ pr/<番号> が、別の記録を指してすでにあるため、この版を開きませんでした。ブランチはそのままです。「ローカルの変更」で切り替えるか、Git で名前を変えるか消してから、もう一度ここで開いてください。",
1271
+ openedPullRequest: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で {previous} に切り替えてください。",
1272
+ openedPullRequestDetached: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で別のブランチに切り替えてください。",
1273
+ locked: "ほかの Git の操作が、このリポジトリを使っています(ロックファイル、またはマージ・cherry-pick・revert・rebase の途中)。終わってから、もう一度試してください。",
1274
+ branchName: "この名前は Git のブランチ名に使えません。",
1275
+ pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
1276
+ diverged: "このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。",
1277
+ session: "このリポジトリと操作に必要な権限で GitHub に接続してください。",
1278
+ pushUnknown: "GitHub が記録を受け取ったか分かりません。送り直す前に「GitHub と確かめる」を使ってください。",
1279
+ pushRefresh: "GitHub が記録を受け取りました。「GitHub と確かめる」で状態を更新してください。",
1280
+ rollbackFailed: "一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。",
1281
+ remoteFailed: "GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。",
1282
+ remoteDone: "GitHub との操作が終わりました。",
1283
+ stage: "次の記録に入れる",
1284
+ unstage: "次の記録から外す",
1285
+ stageAll: "すべての変更を次の記録に入れる",
1286
+ commitLabel: "この記録の説明",
1287
+ commitButton: "記録する(コミット)",
1288
+ committed: "変更を記録しました(コミット)。",
1289
+ commitAuthor: "記録する人",
1290
+ saveFirst: "次の記録に入れる前に、作品を保存してください。",
1291
+ author: "記録する前に、Git でこのリポジトリの user.name と user.email を設定してください。Editor は Git の設定を変えません。",
1292
+ nothingStaged: "先に、変更を次の記録に入れてください。",
1293
+ switchTo: "切り替える",
1294
+ currentBranch: "現在",
1295
+ createBranch: "ブランチを作る",
1296
+ newBranch: "新しいブランチの名前",
1297
+ switchAfterCreate: "作ったら切り替える",
1298
+ createFromRemote: "作って切り替える",
1299
+ remoteBranches: "GitHub(origin)のブランチ",
1300
+ switched: "ブランチを切り替えました。「作品を再読込」で、このブランチのファイルを開けます。",
1301
+ branchCreated: "ブランチを作りました。",
1302
+ projectMissing: "開いている作品フォルダは、このブランチにありません。切り替えますか?",
1303
+ pulledProject: "GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。",
1304
+ confirmPush: "このブランチの記録を GitHub に送りますか(push)?",
1305
+ details: "Git の詳細",
1306
+ branch: "ブランチ",
1307
+ head: "HEAD",
1308
+ groups: "Git の区分",
1309
+ added: "追加",
1310
+ modified: "変更",
1311
+ deleted: "削除",
1312
+ gitStaged: "ステージ済み (staged)",
1313
+ gitUnstaged: "未ステージ (unstaged)",
1314
+ gitUntracked: "未追跡 (untracked)",
1315
+ upstreamSame: "GitHub と同じ",
1316
+ upstreamAhead: "GitHub より進んでいる",
1317
+ upstreamBehind: "GitHub より遅れている",
1318
+ upstreamDiverged: "GitHub と分かれている",
1319
+ upstreamUnknown: "まだ GitHub と確かめていない",
1320
+ records: "件",
1321
+ checkedAt: "最後に確かめた時点",
1322
+ failed: "リポジトリを読み込めませんでした。フォルダへのアクセスを確認し、再取得してください。",
1323
+ historyMore: "さらに表示",
1324
+ historyLoading: "記録を読込中…",
1325
+ historyFailed: "記録を読み込めませんでした。",
1326
+ historyActions: "操作",
1327
+ historyCreateHere: "ここからブランチを作る…",
1328
+ historySwitch: "{name} に切り替える",
1329
+ historyCreateFromRemote: "{name} を作って切り替える",
1330
+ historyCopy: "コミットの ID をコピー",
1331
+ historyCopied: "コミットの ID をコピーしました。",
1332
+ historyCopyFailed: "コミットの ID をコピーできませんでした。",
1333
+ historyRefs: "ブランチ",
1334
+ cancel: "キャンセル",
1335
+ branchMenu: "ブランチ",
1336
+ openPanel: "ローカル Git を開く",
1337
+ newBranchItem: "新しいブランチ…",
1338
+ newBranchTitle: "新しいブランチ",
1339
+ switchedShort: "ブランチを切り替えました。",
1340
+ historyViewEmpty: "Git リポジトリを選ぶと、ここに履歴を表示します。"
1341
+ }
1342
+ };
1343
+ var labelsFor = (locale) => locale.startsWith("ja") ? messages.ja : messages.en;
1344
+ /** The words of sending a pull request review through the GitHub connection. */
1345
+ var reviewMessages = {
1346
+ en: {
1347
+ composer: "Send a review",
1348
+ overall: "Overall comment",
1349
+ event: "Result",
1350
+ COMMENT: "Comment",
1351
+ APPROVE: "Approve",
1352
+ REQUEST_CHANGES: "Request changes",
1353
+ scriptComment: "Comment on this code",
1354
+ commented: "Commented",
1355
+ summaryThreads: "Code comments sent as threads on their project.source.json: {threads}. Code comments in the review body: {body}.",
1356
+ summaryBody: "This connection sends only the review body: {body} code comments go in it.",
1357
+ summaryNone: "No code comments yet. Write them under each code change.",
1358
+ send: "Send Review",
1359
+ sending: "Sending the review…",
1360
+ checking: "Checking GitHub…",
1361
+ sendAgain: "Send Again",
1362
+ check: "Check Whether It Was Sent",
1363
+ discard: "Discard the Pending Review",
1364
+ forget: "Stop Tracking This Submission",
1365
+ useVersion: "Review This Version",
1366
+ sent: "Sent the review.",
1367
+ openReview: "Open the review on GitHub",
1368
+ openPullRequest: "Open the pull request on GitHub",
1369
+ dismiss: "Dismiss",
1370
+ discarded: "Discarded the pending review. Your words are kept.",
1371
+ notStored: "Browser storage is unavailable, so this draft is kept only while the panel is open.",
1372
+ headChanged: "The pull request changed after you started this review. The review is sent for the version shown.",
1373
+ unsupported: "This GitHub connection cannot send reviews.",
1374
+ needsGrant: "To send a review, disconnect and connect again with “Allow sending reviews”.",
1375
+ allowReview: "Allow sending reviews",
1376
+ PENDING_REVIEW_EXISTS: "You already have a pending review on this pull request on GitHub. Submit or discard it on GitHub, then send again.",
1377
+ PENDING_REVIEW_LEFT: "A pending review remains on GitHub. Only you can see it. Discard it here, or on GitHub with “Finish your review”.",
1378
+ REVIEW_OUTCOME_UNKNOWN: "It is unknown whether GitHub received the review. Check before sending again.",
1379
+ pending: "The review waits on GitHub with its code comments but is not published. Change it and send again, or discard it.",
1380
+ REVIEW_REJECTED: "GitHub refused the review, for example an approval of your own pull request.",
1381
+ REVIEW_THREAD_REJECTED: "GitHub refused a comment on a project file, so nothing was published. Comments on that file now go in the review body. Send again.",
1382
+ RATE_LIMITED: "The GitHub rate limit was reached. Try again in {seconds} seconds.",
1383
+ EMPTY_REVIEW: "Write an overall comment (needed to request changes) or a comment on the code.",
1384
+ TOO_LONG: "The review is longer than GitHub accepts (65,536 characters).",
1385
+ closed: "GitHub has no such review: it was not published, or it was discarded. You can send it again.",
1386
+ lost: "The connection no longer knows this submission. Check on GitHub whether the review was published ({count} of your reviews on this version found).",
1387
+ session: "Connect to GitHub again to send the review.",
1388
+ FORBIDDEN: "The GitHub connection is not allowed to send reviews to this repository.",
1389
+ GITHUB_API_UNAVAILABLE: "The connection cannot add code comments as threads. They now go in the review body: check the summary and send again.",
1390
+ failed: "Could not send the review. Nothing was published. Try again."
1391
+ },
1392
+ ja: {
1393
+ composer: "レビューを送る",
1394
+ overall: "全体のコメント",
1395
+ event: "結果",
1396
+ COMMENT: "コメント",
1397
+ APPROVE: "承認",
1398
+ REQUEST_CHANGES: "修正を依頼",
1399
+ scriptComment: "このコードへのコメント",
1400
+ commented: "コメントあり",
1401
+ summaryThreads: "コードへのコメントのうち {threads} 件をその project.source.json へのスレッドとして、{body} 件をレビュー本文に入れて送ります。",
1402
+ summaryBody: "この接続ではレビュー本文だけを送れます。コードへのコメント {body} 件を本文に入れます。",
1403
+ summaryNone: "コードへのコメントはまだありません。各コードの変更の下に書けます。",
1404
+ send: "レビューを送信",
1405
+ sending: "レビューを送信中…",
1406
+ checking: "GitHub で確かめています…",
1407
+ sendAgain: "もう一度送る",
1408
+ check: "送信されたか確かめる",
1409
+ discard: "保留中のレビューを破棄",
1410
+ forget: "この送信の記録を消す",
1411
+ useVersion: "この版をレビューする",
1412
+ sent: "レビューを送りました。",
1413
+ openReview: "GitHub でレビューを開く",
1414
+ openPullRequest: "GitHub で PR を開く",
1415
+ dismiss: "閉じる",
1416
+ discarded: "保留中のレビューを破棄しました。書いた内容は残っています。",
1417
+ notStored: "ブラウザーのストレージを使えないため、この下書きはパネルを開いている間だけ残ります。",
1418
+ headChanged: "レビューを書き始めた後に PR が更新されました。レビューは表示中の版に対して送ります。",
1419
+ unsupported: "この GitHub 接続ではレビューを送れません。",
1420
+ needsGrant: "レビューを送るには、切断して「レビューの送信を許可する」を付けて接続し直してください。",
1421
+ allowReview: "レビューの送信を許可する",
1422
+ PENDING_REVIEW_EXISTS: "この PR に、GitHub で書きかけの保留中のレビューがあります。GitHub で送るか破棄してから、もう一度送ってください。",
1423
+ PENDING_REVIEW_LEFT: "GitHub に保留中のレビューが残っています。あなたにしか見えません。ここで破棄するか、GitHub の「Finish your review」から破棄してください。",
1424
+ REVIEW_OUTCOME_UNKNOWN: "GitHub がレビューを受け取ったかわかりません。もう一度送る前に確かめてください。",
1425
+ pending: "レビューはコードへのコメント付きで GitHub に保留中ですが、公開されていません。直してもう一度送るか、破棄してください。",
1426
+ REVIEW_REJECTED: "GitHub がレビューを拒みました(自分の PR の承認など)。",
1427
+ REVIEW_THREAD_REJECTED: "GitHub が作品ファイルへのコメントを拒んだため、何も公開していません。そのファイルへのコメントはレビュー本文に入れます。もう一度送ってください。",
1428
+ RATE_LIMITED: "GitHub の利用上限に達しました。{seconds} 秒後に再試行してください。",
1429
+ EMPTY_REVIEW: "全体のコメント(修正の依頼では必須)か、コードへのコメントを書いてください。",
1430
+ TOO_LONG: "レビューが GitHub の上限(65,536 文字)を超えています。",
1431
+ closed: "GitHub にそのレビューはありません。公開されなかったか、破棄されています。もう一度送れます。",
1432
+ lost: "接続がこの送信の記録を持っていません。レビューが公開されたかを GitHub で確かめてください(この版へのあなたのレビュー {count} 件)。",
1433
+ session: "レビューを送るには、GitHub にもう一度接続してください。",
1434
+ FORBIDDEN: "GitHub 接続に、このリポジトリへレビューを送る権限がありません。",
1435
+ GITHUB_API_UNAVAILABLE: "この接続ではコードへのコメントをスレッドにできません。コメントはレビュー本文に入れます。件数を確かめて、もう一度送ってください。",
1436
+ failed: "レビューを送れませんでした。何も公開していません。再試行してください。"
1437
+ }
1438
+ };
1439
+ var reviewLabelsFor = (locale) => locale.startsWith("ja") ? reviewMessages.ja : reviewMessages.en;
1440
+ // src/review-submit.ts
1441
+ init_objectSpread2();
1442
+ var emptyReviewDraft = () => ({
1443
+ event: "COMMENT",
1444
+ body: "",
1445
+ comments: {},
1446
+ inBody: []
1447
+ });
1448
+ var draftKey = (name) => `turbowarp-plus-git:review-draft:${name}`;
1449
+ var memoryDrafts = /* @__PURE__ */ new Map();
1450
+ var draftListeners = /* @__PURE__ */ new Map();
1451
+ var persistent = true;
1452
+ var running = /* @__PURE__ */ new Set();
1453
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1454
+ var isSha$1 = (value) => typeof value === "string" && /^[0-9a-f]{40}$/u.test(value);
1455
+ var states = [
1456
+ "starting",
1457
+ "threading",
1458
+ "submitting",
1459
+ "start-unknown",
1460
+ "submit-unknown",
1461
+ "pending",
1462
+ "left"
1463
+ ];
1464
+ /** Keeps only the fields of a draft, so nothing else read from storage reaches a request. */
1465
+ function readDraftValue(value, settle) {
1466
+ if (!isRecord(value)) return null;
1467
+ const draft = emptyReviewDraft();
1468
+ if (reviewEvents.includes(value.event)) draft.event = value.event;
1469
+ if (typeof value.body === "string") draft.body = value.body;
1470
+ if (isRecord(value.comments)) {
1471
+ for (const [key, comment] of Object.entries(value.comments)) if (isRecord(comment) && typeof comment.file === "string" && typeof comment.heading === "string" && typeof comment.body === "string") draft.comments[key] = {
1472
+ file: comment.file,
1473
+ heading: comment.heading,
1474
+ body: comment.body
1475
+ };
1476
+ }
1477
+ if (Array.isArray(value.inBody)) draft.inBody = value.inBody.filter((file) => typeof file === "string");
1478
+ if (isSha$1(value.head)) draft.head = value.head;
1479
+ const submit = value.submit;
1480
+ if (isRecord(submit) && typeof submit.id === "string" && isSha$1(submit.commitOid) && (submit.route === "threads" || submit.route === "body") && states.includes(submit.state) && Array.isArray(submit.threadKeys)) {
1481
+ const state = submit.state;
1482
+ draft.submit = _objectSpread2({
1483
+ id: submit.id,
1484
+ commitOid: submit.commitOid,
1485
+ route: submit.route,
1486
+ state: !settle ? state : state === "starting" ? "start-unknown" : state === "threading" ? "left" : state === "submitting" ? "submit-unknown" : state,
1487
+ threadKeys: submit.threadKeys.filter((key) => typeof key === "string")
1488
+ }, typeof submit.sent === "string" && { sent: submit.sent });
1489
+ }
1490
+ return draft;
1491
+ }
1492
+ /** The saved draft of a review, such as `owner/repo#42`, or an empty one. */
1493
+ function readReviewDraft(name) {
1494
+ const memory = memoryDrafts.get(name);
1495
+ if (memory) return memory;
1496
+ try {
1497
+ var _globalThis$localStor;
1498
+ const stored = (_globalThis$localStor = globalThis.localStorage) === null || _globalThis$localStor === void 0 ? void 0 : _globalThis$localStor.getItem(draftKey(name));
1499
+ return stored && readDraftValue(JSON.parse(stored), !running.has(name)) || emptyReviewDraft();
1500
+ } catch (_unused) {
1501
+ return emptyReviewDraft();
1502
+ }
1503
+ }
1504
+ var isEmpty = (draft) => !draft.body && !draft.submit && !draft.inBody.length && Object.values(draft.comments).every((comment) => !comment.body);
1505
+ /** Saves a draft, or removes it with `null`. Without browser storage, the draft is kept while the page is open. */
1506
+ function writeReviewDraft(name, draft) {
1507
+ var _draftListeners$get;
1508
+ const value = draft && !isEmpty(draft) ? draft : null;
1509
+ try {
1510
+ if (!globalThis.localStorage) throw new Error();
1511
+ if (value) globalThis.localStorage.setItem(draftKey(name), JSON.stringify(value));
1512
+ else globalThis.localStorage.removeItem(draftKey(name));
1513
+ memoryDrafts.delete(name);
1514
+ persistent = true;
1515
+ } catch (_unused2) {
1516
+ persistent = false;
1517
+ if (value) memoryDrafts.set(name, value);
1518
+ else memoryDrafts.delete(name);
1519
+ }
1520
+ (_draftListeners$get = draftListeners.get(name)) === null || _draftListeners$get === void 0 || _draftListeners$get.forEach((listener) => listener());
1521
+ }
1522
+ /** Whether the last write reached browser storage. */
1523
+ var reviewDraftPersistent = () => persistent;
1524
+ function subscribeReviewDraft(name, listener) {
1525
+ var _draftListeners$get2;
1526
+ const listeners = (_draftListeners$get2 = draftListeners.get(name)) !== null && _draftListeners$get2 !== void 0 ? _draftListeners$get2 : /* @__PURE__ */ new Set();
1527
+ draftListeners.set(name, listeners.add(listener));
1528
+ return () => {
1529
+ listeners.delete(listener);
1530
+ if (!listeners.size) draftListeners.delete(name);
1531
+ };
1532
+ }
1533
+ var threadBody = (comment) => `**${comment.heading}**\n\n${comment.body.trim()}`;
1534
+ /** Puts the overall comment first, then each script comment that is not a thread under its file and heading. */
1535
+ function reviewBody(overall, comments) {
1536
+ const parts = overall.trim() ? [overall.trim()] : [];
1537
+ if (comments.length) {
1538
+ if (parts.length) parts.push("---");
1539
+ for (const { comment } of comments) parts.push(`**${comment.file} · ${comment.heading}**\n\n${comment.body.trim()}`);
1540
+ }
1541
+ return parts.join("\n\n");
1542
+ }
1543
+ /**
1544
+ * Decides where each script comment goes. A comment becomes a thread when threads are used, its project.source.json is
1545
+ * among the pull request's changed files, GitHub has not refused a thread on that file, and fewer than 50 threads come
1546
+ * before it. Comments in `exclude` are already on the pending review.
1547
+ */
1548
+ function planReview(draft, { threads, changedFiles, exclude = [] }) {
1549
+ const changed = new Set(changedFiles);
1550
+ const plan = {
1551
+ threads: [],
1552
+ inBody: [],
1553
+ body: ""
1554
+ };
1555
+ const comments = Object.entries(draft.comments).filter(([key, comment]) => comment.body.trim() && !exclude.includes(key)).sort(([a, x], [b, y]) => x.file < y.file ? -1 : x.file > y.file ? 1 : a < b ? -1 : a > b ? 1 : 0);
1556
+ for (const [key, comment] of comments) if (threads && plan.threads.length < 50 && changed.has(comment.file) && isReviewThreadPath(comment.file) && !draft.inBody.includes(comment.file)) plan.threads.push({
1557
+ key,
1558
+ path: comment.file,
1559
+ body: threadBody(comment)
1560
+ });
1561
+ else plan.inBody.push({
1562
+ key,
1563
+ comment
1564
+ });
1565
+ plan.body = reviewBody(draft.body, plan.inBody);
1566
+ return plan;
1567
+ }
1568
+ var bodyFingerprint = (body) => {
1569
+ let hash = 2166136261;
1570
+ for (let index = 0; index < body.length; index++) hash = Math.imul(hash ^ body.charCodeAt(index), 16777619);
1571
+ return body.length.toString(36) + "." + (hash >>> 0).toString(36);
1572
+ };
1573
+ /** What GitHub would refuse before anything is sent. */
1574
+ function reviewProblem(event, plan, route) {
1575
+ const threads = route === "threads" ? plan.threads.length : 0;
1576
+ if (plan.body.length > 65536 || plan.threads.some((thread) => thread.body.length > 65536)) return "TOO_LONG";
1577
+ if (event === "REQUEST_CHANGES" && !plan.body) return "EMPTY_REVIEW";
1578
+ if (event === "COMMENT" && !plan.body && !threads) return "EMPTY_REVIEW";
1579
+ return null;
1580
+ }
1581
+ /** Runs one action at a time per review, so the steps of a submission are never sent side by side. */
1582
+ async function exclusive(target, action) {
1583
+ if (running.has(target.name)) return {
1584
+ outcome: "not-sent",
1585
+ code: "BUSY"
1586
+ };
1587
+ running.add(target.name);
1588
+ try {
1589
+ return await action();
1590
+ } finally {
1591
+ running.delete(target.name);
1592
+ }
1593
+ }
1594
+ var failure = (error) => error instanceof GitHubReviewError ? _objectSpread2({ code: error.code }, error.retryAfter !== void 0 && { retryAfter: error.retryAfter }) : { code: "NETWORK_ERROR" };
1595
+ var isUnknown = (error) => error instanceof GitHubReviewError ? error.unknown : true;
1596
+ var codeOf = (error) => error instanceof GitHubReviewError ? error.code : void 0;
1597
+ function update(name, change) {
1598
+ writeReviewDraft(name, change(readReviewDraft(name)));
1599
+ }
1600
+ var setSubmit = (name, submit) => update(name, (draft) => _objectSpread2(_objectSpread2({}, draft), {}, { submit }));
1601
+ var finish = (name) => writeReviewDraft(name, null);
1602
+ /** The route the next submission takes, or `null` when the proxy offers neither. */
1603
+ function reviewRoute(api) {
1604
+ return api.available.threads !== false ? "threads" : api.available.body !== false ? "body" : null;
1605
+ }
1606
+ /**
1607
+ * Asks the proxy which review routes it offers, with one read: the review context, then, when the GraphQL routes are
1608
+ * off, the list of reviews. A failure other than an unavailable route leaves the route as it was.
1609
+ */
1610
+ async function probeReviewRoutes(api, number, signal) {
1611
+ let context = null;
1612
+ try {
1613
+ context = await api.context(number, void 0, signal);
1614
+ } catch (error) {
1615
+ if (codeOf(error) !== "GITHUB_API_UNAVAILABLE") return {
1616
+ context: null,
1617
+ error
1618
+ };
1619
+ }
1620
+ if (api.available.threads === false && api.available.body === void 0) try {
1621
+ await api.reviews(number, 1, signal);
1622
+ } catch (error) {
1623
+ if (codeOf(error) !== "GITHUB_API_UNAVAILABLE") return {
1624
+ context,
1625
+ error
1626
+ };
1627
+ }
1628
+ return { context };
1629
+ }
1630
+ /** Discards the pending review after a failed step. The draft keeps the words either way. */
1631
+ async function discardAfter(target, submit, cause) {
1632
+ const { api, name, number, signal } = target;
1633
+ try {
1634
+ await api.discard(number, submit.id, signal);
1635
+ } catch (error) {
1636
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "left" }));
1637
+ return _objectSpread2({
1638
+ outcome: "left",
1639
+ code: "PENDING_REVIEW_LEFT"
1640
+ }, failure(error).retryAfter !== void 0 && { retryAfter: failure(error).retryAfter });
1641
+ }
1642
+ setSubmit(name, void 0);
1643
+ return _objectSpread2({ outcome: "not-sent" }, failure(cause));
1644
+ }
1645
+ async function lastStep(target, submit, draft) {
1646
+ const { api, name, number, signal } = target;
1647
+ const plan = planReview(draft, {
1648
+ threads: false,
1649
+ changedFiles: [],
1650
+ exclude: submit.threadKeys
1651
+ });
1652
+ const problem = reviewProblem(draft.event, _objectSpread2(_objectSpread2({}, plan), {}, { threads: submit.threadKeys.map((key) => ({
1653
+ key,
1654
+ path: "",
1655
+ body: ""
1656
+ })) }), "threads");
1657
+ if (problem) {
1658
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" }));
1659
+ return {
1660
+ outcome: "pending",
1661
+ code: problem
1662
+ };
1663
+ }
1664
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "submitting" }));
1665
+ try {
1666
+ const published = await api.submit(number, submit.id, submit.threadKeys.length, draft.event, plan.body, signal);
1667
+ finish(name);
1668
+ return {
1669
+ outcome: "submitted",
1670
+ url: published.url
1671
+ };
1672
+ } catch (error) {
1673
+ if (isUnknown(error)) {
1674
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "submit-unknown" }));
1675
+ return {
1676
+ outcome: "unknown",
1677
+ code: "REVIEW_OUTCOME_UNKNOWN"
1678
+ };
1679
+ }
1680
+ if (codeOf(error) === "REVIEW_STEP_CONFLICT") {
1681
+ setSubmit(name, void 0);
1682
+ return _objectSpread2({ outcome: "not-sent" }, failure(error));
1683
+ }
1684
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" }));
1685
+ return _objectSpread2({ outcome: "pending" }, failure(error));
1686
+ }
1687
+ }
1688
+ async function sendThreads(target, draft) {
1689
+ const { api, name, number, head, changedFiles, signal } = target;
1690
+ const plan = planReview(draft, {
1691
+ threads: true,
1692
+ changedFiles
1693
+ });
1694
+ const problem = reviewProblem(draft.event, plan, "threads");
1695
+ if (problem) return {
1696
+ outcome: "not-sent",
1697
+ code: problem
1698
+ };
1699
+ let submit = {
1700
+ id: newReviewSubmitId(),
1701
+ commitOid: head,
1702
+ route: "threads",
1703
+ state: "starting",
1704
+ threadKeys: []
1705
+ };
1706
+ setSubmit(name, submit);
1707
+ try {
1708
+ await api.start(number, submit.id, head, signal);
1709
+ } catch (error) {
1710
+ if (!isUnknown(error)) {
1711
+ setSubmit(name, void 0);
1712
+ return _objectSpread2({ outcome: "not-sent" }, failure(error));
1713
+ }
1714
+ submit = _objectSpread2(_objectSpread2({}, submit), {}, { state: "start-unknown" });
1715
+ setSubmit(name, submit);
1716
+ return resolveStart(target, submit, error);
1717
+ }
1718
+ submit = _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" });
1719
+ setSubmit(name, submit);
1720
+ for (const [index, thread] of plan.threads.entries()) {
1721
+ submit = _objectSpread2(_objectSpread2({}, submit), {}, { state: "threading" });
1722
+ setSubmit(name, submit);
1723
+ try {
1724
+ await api.thread(number, submit.id, index, thread.path, thread.body, signal);
1725
+ } catch (error) {
1726
+ if (codeOf(error) === "REVIEW_THREAD_REJECTED") update(name, (current) => _objectSpread2(_objectSpread2({}, current), {}, { inBody: [.../* @__PURE__ */ new Set([...current.inBody, thread.path])] }));
1727
+ if (codeOf(error) === "REVIEW_STEP_CONFLICT") {
1728
+ setSubmit(name, void 0);
1729
+ return _objectSpread2({ outcome: "not-sent" }, failure(error));
1730
+ }
1731
+ return discardAfter(target, submit, error);
1732
+ }
1733
+ submit = _objectSpread2(_objectSpread2({}, submit), {}, {
1734
+ state: "pending",
1735
+ threadKeys: [...submit.threadKeys, thread.key]
1736
+ });
1737
+ setSubmit(name, submit);
1738
+ }
1739
+ submit = _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" });
1740
+ return lastStep(target, submit, readReviewDraft(name));
1741
+ }
1742
+ /** After an unknown first step: a pending review that the step created is discarded. */
1743
+ async function resolveStart(target, submit, cause) {
1744
+ const { api, name, number, signal } = target;
1745
+ let state;
1746
+ try {
1747
+ var _await$api$context$su;
1748
+ state = (_await$api$context$su = (await api.context(number, submit.id, signal)).submit) === null || _await$api$context$su === void 0 ? void 0 : _await$api$context$su.state;
1749
+ } catch (error) {
1750
+ if (codeOf(error) === "REVIEW_STEP_CONFLICT") return lost(target, submit);
1751
+ return {
1752
+ outcome: "unknown",
1753
+ code: "REVIEW_OUTCOME_UNKNOWN"
1754
+ };
1755
+ }
1756
+ if (state === "closed") {
1757
+ setSubmit(name, void 0);
1758
+ return _objectSpread2({ outcome: "not-sent" }, failure(cause));
1759
+ }
1760
+ if (state === "pending" || state === "broken") return discardAfter(target, _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" }), cause);
1761
+ return {
1762
+ outcome: "unknown",
1763
+ code: "REVIEW_OUTCOME_UNKNOWN"
1764
+ };
1765
+ }
1766
+ async function lost(target, submit) {
1767
+ let reviews = [];
1768
+ try {
1769
+ reviews = (await target.api.context(target.number, void 0, target.signal)).reviews.filter((review) => review.commitOid === submit.commitOid).map(({ state, submittedAt }) => ({
1770
+ state,
1771
+ submittedAt
1772
+ }));
1773
+ } catch (_unused3) {}
1774
+ return {
1775
+ outcome: "lost",
1776
+ code: "REVIEW_STEP_CONFLICT",
1777
+ reviews
1778
+ };
1779
+ }
1780
+ async function sendBody(target, draft) {
1781
+ const { api, name, number, head, signal } = target;
1782
+ const plan = planReview(draft, {
1783
+ threads: false,
1784
+ changedFiles: []
1785
+ });
1786
+ const problem = reviewProblem(draft.event, plan, "body");
1787
+ if (problem) return {
1788
+ outcome: "not-sent",
1789
+ code: problem
1790
+ };
1791
+ const submit = {
1792
+ id: newReviewSubmitId(),
1793
+ commitOid: head,
1794
+ route: "body",
1795
+ state: "submitting",
1796
+ threadKeys: [],
1797
+ sent: bodyFingerprint(plan.body)
1798
+ };
1799
+ setSubmit(name, submit);
1800
+ try {
1801
+ const created = await api.create(number, submit.id, head, draft.event, plan.body, signal);
1802
+ finish(name);
1803
+ return {
1804
+ outcome: "submitted",
1805
+ url: created.url
1806
+ };
1807
+ } catch (error) {
1808
+ if (isUnknown(error)) {
1809
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "submit-unknown" }));
1810
+ return {
1811
+ outcome: "unknown",
1812
+ code: "REVIEW_OUTCOME_UNKNOWN"
1813
+ };
1814
+ }
1815
+ setSubmit(name, void 0);
1816
+ return _objectSpread2({ outcome: "not-sent" }, failure(error));
1817
+ }
1818
+ }
1819
+ /**
1820
+ * Sends the draft, or, after the last step was refused (`pending`), sends that step again with the current event and
1821
+ * overall comment. A submission whose outcome is unknown, or whose pending review was left, is not sent again.
1822
+ */
1823
+ function sendReview(target) {
1824
+ return exclusive(target, async () => {
1825
+ const draft = readReviewDraft(target.name);
1826
+ const submit = draft.submit;
1827
+ if ((submit === null || submit === void 0 ? void 0 : submit.state) === "pending" && submit.route === "threads") return lastStep(target, submit, draft);
1828
+ if ((submit === null || submit === void 0 ? void 0 : submit.state) === "left") return {
1829
+ outcome: "left",
1830
+ code: "PENDING_REVIEW_LEFT"
1831
+ };
1832
+ if (submit) return {
1833
+ outcome: "unknown",
1834
+ code: "REVIEW_OUTCOME_UNKNOWN"
1835
+ };
1836
+ const route = reviewRoute(target.api);
1837
+ if (!route) return {
1838
+ outcome: "not-sent",
1839
+ code: "UNSUPPORTED"
1840
+ };
1841
+ const result = await (route === "threads" ? sendThreads(target, draft) : sendBody(target, draft));
1842
+ if (result.code === "GITHUB_API_UNAVAILABLE" && !reviewRoute(target.api)) return _objectSpread2(_objectSpread2({}, result), {}, { code: "UNSUPPORTED" });
1843
+ return result;
1844
+ });
1845
+ }
1846
+ /** Finds out what happened to a submission whose outcome is unknown. Reading is safe to repeat. */
1847
+ function checkReview(target) {
1848
+ return exclusive(target, async () => {
1849
+ const { api, name, number, signal } = target;
1850
+ const submit = readReviewDraft(name).submit;
1851
+ if (!submit) return { outcome: "not-sent" };
1852
+ if (submit.route === "body") {
1853
+ const login = api.account();
1854
+ try {
1855
+ for (let page = 1; page <= 10; page++) {
1856
+ const reviews = await api.reviews(number, page, signal);
1857
+ if (reviews.some((review) => review.login === login && review.commitId === submit.commitOid && bodyFingerprint(review.body) === submit.sent)) {
1858
+ finish(name);
1859
+ return { outcome: "submitted" };
1860
+ }
1861
+ if (reviews.length < 30) break;
1862
+ }
1863
+ } catch (error) {
1864
+ return _objectSpread2({ outcome: "unknown" }, failure(error));
1865
+ }
1866
+ setSubmit(name, void 0);
1867
+ return { outcome: "closed" };
1868
+ }
1869
+ if (submit.state === "start-unknown") return resolveStart(target, submit, new GitHubReviewError("UPSTREAM_FAILURE"));
1870
+ let state;
1871
+ try {
1872
+ var _await$api$context$su2;
1873
+ state = (_await$api$context$su2 = (await api.context(number, submit.id, signal)).submit) === null || _await$api$context$su2 === void 0 ? void 0 : _await$api$context$su2.state;
1874
+ } catch (error) {
1875
+ if (codeOf(error) === "REVIEW_STEP_CONFLICT") return lost(target, submit);
1876
+ return _objectSpread2({ outcome: "unknown" }, failure(error));
1877
+ }
1878
+ if (state === "submitted") {
1879
+ finish(name);
1880
+ return { outcome: "submitted" };
1881
+ }
1882
+ if (state === "closed") {
1883
+ setSubmit(name, void 0);
1884
+ return { outcome: "closed" };
1885
+ }
1886
+ if (state === "pending") {
1887
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "pending" }));
1888
+ return { outcome: "pending" };
1889
+ }
1890
+ if (state === "broken") {
1891
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "left" }));
1892
+ return {
1893
+ outcome: "left",
1894
+ code: "PENDING_REVIEW_LEFT"
1895
+ };
1896
+ }
1897
+ return {
1898
+ outcome: "unknown",
1899
+ code: "REVIEW_OUTCOME_UNKNOWN"
1900
+ };
1901
+ });
1902
+ }
1903
+ /** Discards the submission's pending review on GitHub, when one was left or the last step was refused. */
1904
+ function discardReview(target) {
1905
+ return exclusive(target, async () => {
1906
+ const { api, name, number, signal } = target;
1907
+ const submit = readReviewDraft(name).submit;
1908
+ if (!submit || submit.route !== "threads" || submit.state !== "pending" && submit.state !== "left") return {
1909
+ outcome: "not-sent",
1910
+ code: "NOT_ALLOWED"
1911
+ };
1912
+ try {
1913
+ await api.discard(number, submit.id, signal);
1914
+ } catch (error) {
1915
+ if (codeOf(error) === "REVIEW_DUPLICATE" || codeOf(error) === "REVIEW_STEP_CONFLICT") try {
1916
+ var _await$api$context$su3;
1917
+ if (((_await$api$context$su3 = (await api.context(number, submit.id, signal)).submit) === null || _await$api$context$su3 === void 0 ? void 0 : _await$api$context$su3.state) === "closed") {
1918
+ setSubmit(name, void 0);
1919
+ return { outcome: "not-sent" };
1920
+ }
1921
+ } catch (check) {
1922
+ if (codeOf(check) === "REVIEW_STEP_CONFLICT") return lost(target, submit);
1923
+ }
1924
+ setSubmit(name, _objectSpread2(_objectSpread2({}, submit), {}, { state: "left" }));
1925
+ return _objectSpread2({
1926
+ outcome: "left",
1927
+ code: "PENDING_REVIEW_LEFT"
1928
+ }, failure(error).retryAfter !== void 0 && { retryAfter: failure(error).retryAfter });
1929
+ }
1930
+ setSubmit(name, void 0);
1931
+ return { outcome: "not-sent" };
1932
+ });
1933
+ }
1934
+ /** Forgets a submission the proxy no longer knows, keeping the words. Anything left on GitHub is handled there. */
1935
+ function forgetReviewSubmission(name) {
1936
+ setSubmit(name, void 0);
1937
+ }
1938
+ // src/review-composer.tsx
1939
+ init_objectSpread2();
1940
+ /** The saved draft of a review, updated when any view of the same review writes it. */
1941
+ function useReviewDraft(name) {
1942
+ const [, refresh] = useState(0);
1943
+ useEffect(() => name ? subscribeReviewDraft(name, () => refresh((count) => count + 1)) : void 0, [name]);
1944
+ return name ? readReviewDraft(name) : null;
1945
+ }
1946
+ /** Changes the saved draft from what is stored now, so other views' words are kept. */
1947
+ function changeReviewDraft(name, change) {
1948
+ writeReviewDraft(name, change(readReviewDraft(name)));
1949
+ }
1950
+ var sessionCodes = [
1951
+ "SESSION_REQUIRED",
1952
+ "SESSION_EXPIRED",
1953
+ "REAUTH_REQUIRED",
1954
+ "SESSION_UNAVAILABLE",
1955
+ "INVALID_SESSION"
1956
+ ];
1957
+ function resultMessage(result, labels) {
1958
+ var _result$retryAfter, _result$reviews$lengt, _result$reviews;
1959
+ const code = result.code;
1960
+ if (code === "RATE_LIMITED") return labels.RATE_LIMITED.replace("{seconds}", String((_result$retryAfter = result.retryAfter) !== null && _result$retryAfter !== void 0 ? _result$retryAfter : 60));
1961
+ if (result.outcome === "submitted") return labels.sent;
1962
+ if (result.outcome === "closed") return labels.closed;
1963
+ if (result.outcome === "lost") return labels.lost.replace("{count}", String((_result$reviews$lengt = (_result$reviews = result.reviews) === null || _result$reviews === void 0 ? void 0 : _result$reviews.length) !== null && _result$reviews$lengt !== void 0 ? _result$reviews$lengt : 0));
1964
+ if (result.outcome === "left") return labels.PENDING_REVIEW_LEFT;
1965
+ if (result.outcome === "unknown") return labels.REVIEW_OUTCOME_UNKNOWN;
1966
+ if (code && sessionCodes.includes(code)) return labels.session;
1967
+ if (code === "PENDING_REVIEW_EXISTS" || code === "REVIEW_REJECTED" || code === "REVIEW_THREAD_REJECTED" || code === "EMPTY_REVIEW" || code === "TOO_LONG" || code === "FORBIDDEN" || code === "GITHUB_API_UNAVAILABLE") {
1968
+ const message = labels[code];
1969
+ return result.outcome === "pending" ? labels.pending + " " + message : message;
1970
+ }
1971
+ if (code === "UNSUPPORTED") return labels.unsupported;
1972
+ if (result.outcome === "pending") return labels.pending;
1973
+ if (result.outcome === "not-sent" && !code) return labels.discarded;
1974
+ return labels.failed;
1975
+ }
1976
+ /**
1977
+ * The overall comment, the result, how the code comments will be sent, and the send button of a pull request review.
1978
+ * The code comments are written under each code change of the review that shares `name`.
1979
+ */
1980
+ function PullRequestReviewComposer({ api, name, number, head, changedFiles, url, locale }) {
1981
+ const labels = reviewLabelsFor(locale);
1982
+ const draft = useReviewDraft(name);
1983
+ const [route, setRoute] = useState(() => reviewRoute(api));
1984
+ const [busy, setBusy] = useState(null);
1985
+ const [result, setResult] = useState(null);
1986
+ const [blocked, setBlocked] = useState(false);
1987
+ const active = useRef(true);
1988
+ useEffect(() => {
1989
+ active.current = true;
1990
+ return () => {
1991
+ active.current = false;
1992
+ };
1993
+ }, []);
1994
+ const target = {
1995
+ api,
1996
+ name,
1997
+ number,
1998
+ head,
1999
+ changedFiles
2000
+ };
2001
+ const run = async (kind, action) => {
2002
+ if (busy || blocked) return;
2003
+ setBusy(kind);
2004
+ setResult(null);
2005
+ let next;
2006
+ try {
2007
+ next = await action();
2008
+ } catch (_unused) {
2009
+ next = {
2010
+ outcome: "not-sent",
2011
+ code: "NETWORK_ERROR"
2012
+ };
2013
+ }
2014
+ if (!active.current) return;
2015
+ setBusy(null);
2016
+ setRoute(reviewRoute(api));
2017
+ if (next.code === "BUSY") return;
2018
+ setResult(next);
2019
+ if (next.retryAfter) {
2020
+ setBlocked(true);
2021
+ setTimeout(() => {
2022
+ if (active.current) setBlocked(false);
2023
+ }, next.retryAfter * 1e3);
2024
+ }
2025
+ };
2026
+ useEffect(() => {
2027
+ const controller = new AbortController();
2028
+ (async () => {
2029
+ var _readReviewDraft$subm;
2030
+ await probeReviewRoutes(api, number, controller.signal);
2031
+ if (controller.signal.aborted) return;
2032
+ setRoute(reviewRoute(api));
2033
+ const state = (_readReviewDraft$subm = readReviewDraft(name).submit) === null || _readReviewDraft$subm === void 0 ? void 0 : _readReviewDraft$subm.state;
2034
+ if (state === "start-unknown" || state === "submit-unknown") await run("check", () => checkReview(target));
2035
+ })();
2036
+ return () => controller.abort();
2037
+ }, [
2038
+ api,
2039
+ name,
2040
+ number
2041
+ ]);
2042
+ const hasWords = !!draft.body || Object.values(draft.comments).some((comment) => comment.body);
2043
+ useEffect(() => {
2044
+ if (hasWords && !draft.head) changeReviewDraft(name, (current) => _objectSpread2(_objectSpread2({}, current), {}, { head }));
2045
+ }, [
2046
+ hasWords,
2047
+ draft.head,
2048
+ head,
2049
+ name
2050
+ ]);
2051
+ const submit = draft.submit;
2052
+ const plan = planReview(draft, {
2053
+ threads: route === "threads" && (submit === null || submit === void 0 ? void 0 : submit.state) !== "pending",
2054
+ changedFiles,
2055
+ exclude: submit === null || submit === void 0 ? void 0 : submit.threadKeys
2056
+ });
2057
+ const threads = (submit === null || submit === void 0 ? void 0 : submit.state) === "pending" ? submit.threadKeys.length : route === "threads" ? plan.threads.length : 0;
2058
+ const comments = ((submit === null || submit === void 0 ? void 0 : submit.state) === "pending" ? submit.threadKeys.length : 0) + plan.threads.length + plan.inBody.length;
2059
+ const problem = route && reviewProblem(draft.event, _objectSpread2(_objectSpread2({}, plan), {}, { threads: Array.from({ length: threads }, () => ({
2060
+ key: "",
2061
+ path: "",
2062
+ body: ""
2063
+ })) }), route);
2064
+ const disabled = !!busy || blocked;
2065
+ const message = result && resultMessage(result, labels);
2066
+ const failed = result && result.outcome !== "submitted" && !(result.outcome === "not-sent" && !result.code);
2067
+ const pullLink = /* @__PURE__ */ React.createElement("a", {
2068
+ href: url,
2069
+ target: "_blank",
2070
+ rel: "noreferrer"
2071
+ }, labels.openPullRequest);
2072
+ return /* @__PURE__ */ React.createElement("section", {
2073
+ className: "twgit-review-composer",
2074
+ "aria-label": labels.composer,
2075
+ "data-git-review-composer": ""
2076
+ }, /* @__PURE__ */ React.createElement("h3", null, labels.composer), !reviewDraftPersistent() && /* @__PURE__ */ React.createElement("p", {
2077
+ role: "status",
2078
+ className: "twgit-review-note"
2079
+ }, labels.notStored), draft.head && draft.head !== head && /* @__PURE__ */ React.createElement("p", {
2080
+ role: "status",
2081
+ className: "twgit-review-note"
2082
+ }, labels.headChanged, " ", /* @__PURE__ */ React.createElement("button", {
2083
+ type: "button",
2084
+ onClick: () => changeReviewDraft(name, (current) => _objectSpread2(_objectSpread2({}, current), {}, { head }))
2085
+ }, labels.useVersion)), /* @__PURE__ */ React.createElement("label", { className: "twgit-review-field" }, labels.overall, /* @__PURE__ */ React.createElement("textarea", {
2086
+ value: draft.body,
2087
+ rows: 4,
2088
+ onChange: (event) => {
2089
+ const body = event.currentTarget.value;
2090
+ changeReviewDraft(name, (current) => _objectSpread2(_objectSpread2({}, current), {}, { body }));
2091
+ }
2092
+ })), /* @__PURE__ */ React.createElement("fieldset", { className: "twgit-review-events" }, /* @__PURE__ */ React.createElement("legend", null, labels.event), reviewEvents.map((event) => /* @__PURE__ */ React.createElement("label", { key: event }, /* @__PURE__ */ React.createElement("input", {
2093
+ type: "radio",
2094
+ name: `twgit-review-event-${name}`,
2095
+ value: event,
2096
+ checked: draft.event === event,
2097
+ onChange: () => changeReviewDraft(name, (current) => _objectSpread2(_objectSpread2({}, current), {}, { event }))
2098
+ }), " ", labels[event]))), route === null ? /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.unsupported) : /* @__PURE__ */ React.createElement(React.Fragment, null, (!submit || submit.state === "pending") && /* @__PURE__ */ React.createElement("p", {
2099
+ className: "twgit-review-summary",
2100
+ "data-git-review-summary": ""
2101
+ }, !comments ? labels.summaryNone : route === "threads" ? labels.summaryThreads.replace("{threads}", String(threads)).replace("{body}", String(plan.inBody.length)) : labels.summaryBody.replace("{body}", String(plan.inBody.length))), /* @__PURE__ */ React.createElement("div", { className: "twgit-review-actions" }, (!submit || submit.state === "pending") && /* @__PURE__ */ React.createElement("button", {
2102
+ type: "button",
2103
+ "data-git-review-send": "",
2104
+ disabled: disabled || !!problem,
2105
+ title: problem ? resultMessage({
2106
+ outcome: "not-sent",
2107
+ code: problem
2108
+ }, labels) : void 0,
2109
+ onClick: () => void run("send", () => sendReview(target))
2110
+ }, submit ? labels.sendAgain : labels.send), ((submit === null || submit === void 0 ? void 0 : submit.state) === "start-unknown" || (submit === null || submit === void 0 ? void 0 : submit.state) === "submit-unknown") && /* @__PURE__ */ React.createElement("button", {
2111
+ type: "button",
2112
+ disabled,
2113
+ onClick: () => void run("check", () => checkReview(target))
2114
+ }, labels.check), ((submit === null || submit === void 0 ? void 0 : submit.state) === "pending" || (submit === null || submit === void 0 ? void 0 : submit.state) === "left") && /* @__PURE__ */ React.createElement("button", {
2115
+ type: "button",
2116
+ disabled,
2117
+ onClick: () => void run("send", () => discardReview(target))
2118
+ }, labels.discard), ((submit === null || submit === void 0 ? void 0 : submit.state) === "left" || (result === null || result === void 0 ? void 0 : result.outcome) === "lost") && /* @__PURE__ */ React.createElement(React.Fragment, null, pullLink, /* @__PURE__ */ React.createElement("button", {
2119
+ type: "button",
2120
+ disabled,
2121
+ onClick: () => {
2122
+ forgetReviewSubmission(name);
2123
+ setResult(null);
2124
+ }
2125
+ }, labels.forget))), busy && /* @__PURE__ */ React.createElement("p", { role: "status" }, busy === "send" ? labels.sending : labels.checking), !busy && !result && submit && /* @__PURE__ */ React.createElement("p", {
2126
+ role: "status",
2127
+ className: "twgit-review-state"
2128
+ }, submit.state === "pending" ? labels.pending : submit.state === "left" ? labels.PENDING_REVIEW_LEFT : labels.REVIEW_OUTCOME_UNKNOWN), message && result && /* @__PURE__ */ React.createElement("div", {
2129
+ className: failed ? "twgit-review-result twgit-error" : "twgit-review-result twgit-context",
2130
+ role: failed ? "alert" : "status"
2131
+ }, /* @__PURE__ */ React.createElement("p", null, message, result.code === "PENDING_REVIEW_EXISTS" && /* @__PURE__ */ React.createElement(React.Fragment, null, " ", pullLink), result.outcome === "submitted" && result.url && /* @__PURE__ */ React.createElement(React.Fragment, null, " ", /* @__PURE__ */ React.createElement("a", {
2132
+ href: result.url,
2133
+ target: "_blank",
2134
+ rel: "noreferrer"
2135
+ }, labels.openReview))), /* @__PURE__ */ React.createElement(CloseButton, {
2136
+ label: labels.dismiss,
2137
+ onClick: () => setResult(null)
2138
+ }))));
2139
+ }
790
2140
  // src/project-source-review.tsx
791
2141
  init_objectSpread2();
792
2142
  var ja = {
@@ -891,9 +2241,9 @@ var properties = /* @__PURE__ */ new Map([
891
2241
  ]);
892
2242
  function scriptTitle(side, labels) {
893
2243
  const block = side.target.blocks[side.id];
894
- if (!isRecord(block)) return labels.script;
2244
+ if (!isRecord$2(block)) return labels.script;
895
2245
  if (block.opcode === "event_whenbroadcastreceived") {
896
- const field = isRecord(block.fields) ? block.fields.BROADCAST_OPTION : void 0;
2246
+ const field = isRecord$2(block.fields) ? block.fields.BROADCAST_OPTION : void 0;
897
2247
  const message = Array.isArray(field) && typeof field[0] === "string" ? field[0] : "";
898
2248
  if (message) return labels === ja ? `「${message}」${labels.broadcast}` : `“${message}” ${labels.broadcast}`;
899
2249
  }
@@ -947,7 +2297,7 @@ function scriptReviewTitle(script, labels) {
947
2297
  const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
948
2298
  return beforeTitle && afterTitle && beforeTitle !== afterTitle ? beforeTitle + " → " + afterTitle : afterTitle !== null && afterTitle !== void 0 ? afterTitle : beforeTitle;
949
2299
  }
950
- function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location, expanded = false }) {
2300
+ function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location, expanded = false, comment, target }) {
951
2301
  const [open, setOpen] = useState(expanded);
952
2302
  const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
953
2303
  const status = script.kind;
@@ -958,7 +2308,7 @@ function ScriptComparison({ script, labels, reviewed = false, setReviewed, rende
958
2308
  className: "twgit-review-script",
959
2309
  open: expanded || void 0,
960
2310
  onToggle: (event) => setOpen(event.currentTarget.open)
961
- }, /* @__PURE__ */ React.createElement("summary", null, /* @__PURE__ */ React.createElement("span", { className: "twgit-review-status" }, labels[status]), /* @__PURE__ */ React.createElement("strong", null, title), status === "reorganized" && /* @__PURE__ */ React.createElement("small", null, script.before.length, " → ", script.after.length), summary && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-summary" }, summary), reviewed && /* @__PURE__ */ React.createElement("small", null, labels.reviewed)), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-sides" }, ["before", "after"].map((version) => /* @__PURE__ */ React.createElement("div", { key: version }, script[version].length ? script[version].map((side, index, sides) => /* @__PURE__ */ React.createElement(BlockPreview, {
2311
+ }, /* @__PURE__ */ React.createElement("summary", null, /* @__PURE__ */ React.createElement("span", { className: "twgit-review-status" }, labels[status]), /* @__PURE__ */ React.createElement("strong", null, title), status === "reorganized" && /* @__PURE__ */ React.createElement("small", null, script.before.length, " → ", script.after.length), summary && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-summary" }, summary), reviewed && /* @__PURE__ */ React.createElement("small", null, labels.reviewed), (comment === null || comment === void 0 ? void 0 : comment.value) && /* @__PURE__ */ React.createElement("small", { "data-git-review-commented": "" }, reviewLabelsFor(locale).commented)), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-sides" }, ["before", "after"].map((version) => /* @__PURE__ */ React.createElement("div", { key: version }, script[version].length ? script[version].map((side, index, sides) => /* @__PURE__ */ React.createElement(BlockPreview, {
962
2312
  key: side.id,
963
2313
  side,
964
2314
  title: sides.length > 1 ? `${labels[version]} ${index + 1}` : labels[version],
@@ -980,7 +2330,14 @@ function ScriptComparison({ script, labels, reviewed = false, setReviewed, rende
980
2330
  type: "checkbox",
981
2331
  checked: reviewed,
982
2332
  onChange: (event) => setReviewed(event.currentTarget.checked)
983
- }), " ", labels.reviewed)));
2333
+ }), " ", labels.reviewed), comment && /* @__PURE__ */ React.createElement("label", { className: "twgit-review-field twgit-review-comment" }, reviewLabelsFor(locale).scriptComment, /* @__PURE__ */ React.createElement("textarea", {
2334
+ rows: 3,
2335
+ value: comment.value,
2336
+ onChange: (event) => {
2337
+ var _target$name;
2338
+ return comment.set(event.currentTarget.value, `${(target === null || target === void 0 ? void 0 : target.isStage) ? labels.stage : (_target$name = target === null || target === void 0 ? void 0 : target.name) !== null && _target$name !== void 0 ? _target$name : ""} · ${title} · ${script.id}`);
2339
+ }
2340
+ }))));
984
2341
  }
985
2342
  function propertyNames(keys, labels) {
986
2343
  const locale = labels === ja ? "ja" : "en";
@@ -1030,6 +2387,7 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateB
1030
2387
  loadAsset
1031
2388
  }), target.isStage ? labels.stage : target.name, target.scripts.length > 0 && /* @__PURE__ */ React.createElement("small", null, labels.scriptCount(target.scripts.length))), target.status !== "updated" && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-target-status" }, (target.status === "added" ? labels.targetAdded : labels.targetRemoved)(target.name, target.isStage)), target.status === "updated" && target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.map((script) => {
1032
2389
  const key = reviewKey(review === null || review === void 0 ? void 0 : review.file, target, script);
2390
+ const comments = review === null || review === void 0 ? void 0 : review.comments;
1033
2391
  return /* @__PURE__ */ React.createElement(ScriptComparison, {
1034
2392
  key,
1035
2393
  script,
@@ -1038,7 +2396,12 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateB
1038
2396
  loadAsset,
1039
2397
  location,
1040
2398
  reviewed: review === null || review === void 0 ? void 0 : review.keys.has(key),
1041
- setReviewed: review && ((value) => review.set(key, value))
2399
+ setReviewed: review && ((value) => review.set(key, value)),
2400
+ target,
2401
+ comment: comments && {
2402
+ value: comments.get(key),
2403
+ set: (value, heading) => comments.set(key, value, heading)
2404
+ }
1042
2405
  });
1043
2406
  }))));
1044
2407
  }
@@ -1065,8 +2428,8 @@ function assetOwner(path, status, entries) {
1065
2428
  for (const kind of ["costumes", "sounds"]) {
1066
2429
  const list = owner[kind];
1067
2430
  if (!Array.isArray(list)) continue;
1068
- const entry = list.find((item) => isRecord(item) && assetFileName(item) === name);
1069
- if (isRecord(entry)) return {
2431
+ const entry = list.find((item) => isRecord$2(item) && assetFileName(item) === name);
2432
+ if (isRecord$2(entry)) return {
1070
2433
  target,
1071
2434
  version,
1072
2435
  md5ext: name,
@@ -1193,8 +2556,25 @@ var writeMarks = (name, marks, remember = true) => {
1193
2556
  } catch (_unused4) {}
1194
2557
  (_markListeners$get2 = markListeners.get(name)) === null || _markListeners$get2 === void 0 || _markListeners$get2.forEach((listener) => listener());
1195
2558
  };
1196
- function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en", rememberAs }) {
2559
+ function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en", rememberAs, commentAs }) {
1197
2560
  const labels = locale.startsWith("ja") ? ja : en;
2561
+ const draft = useReviewDraft(commentAs);
2562
+ const commentsFor = (file) => commentAs && draft ? {
2563
+ get: (key) => {
2564
+ var _draft$comments$key$b, _draft$comments$key;
2565
+ return (_draft$comments$key$b = (_draft$comments$key = draft.comments[key]) === null || _draft$comments$key === void 0 ? void 0 : _draft$comments$key.body) !== null && _draft$comments$key$b !== void 0 ? _draft$comments$key$b : "";
2566
+ },
2567
+ set: (key, body, heading) => changeReviewDraft(commentAs, (current) => {
2568
+ const comments = _objectSpread2({}, current.comments);
2569
+ if (body) comments[key] = {
2570
+ file,
2571
+ heading,
2572
+ body
2573
+ };
2574
+ else delete comments[key];
2575
+ return _objectSpread2(_objectSpread2({}, current), {}, { comments });
2576
+ })
2577
+ } : void 0;
1198
2578
  const entries = useMemo(() => files.map((file) => {
1199
2579
  if (!("patch" in file)) return { file };
1200
2580
  try {
@@ -1267,7 +2647,8 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1267
2647
  review: {
1268
2648
  file: file.path,
1269
2649
  keys: reviewed,
1270
- set: setReviewed
2650
+ set: setReviewed,
2651
+ comments: commentsFor(file.path)
1271
2652
  }
1272
2653
  }))));
1273
2654
  }
@@ -1374,15 +2755,15 @@ async function readText(response, limit) {
1374
2755
  return chunks.join("") + decoder.decode();
1375
2756
  }
1376
2757
  function readSummary(value) {
1377
- if (!isRecord(value) || !Number.isSafeInteger(value.number) || Number(value.number) < 1 || typeof value.title !== "string") throw invalidResponse();
2758
+ if (!isRecord$2(value) || !Number.isSafeInteger(value.number) || Number(value.number) < 1 || typeof value.title !== "string") throw invalidResponse();
1378
2759
  return {
1379
2760
  number: Number(value.number),
1380
2761
  title: value.title
1381
2762
  };
1382
2763
  }
1383
2764
  function readRevision(value) {
1384
- if (!isRecord(value) || !isSha(value.sha) || typeof value.ref !== "string") throw invalidResponse();
1385
- const repository = isRecord(value.repo) ? value.repo.full_name : null;
2765
+ if (!isRecord$2(value) || !isSha(value.sha) || typeof value.ref !== "string") throw invalidResponse();
2766
+ const repository = isRecord$2(value.repo) ? value.repo.full_name : null;
1386
2767
  if (repository !== null && !validRepository(repository)) throw invalidResponse();
1387
2768
  return {
1388
2769
  sha: value.sha,
@@ -1392,8 +2773,8 @@ function readRevision(value) {
1392
2773
  }
1393
2774
  function readPullRequest(value) {
1394
2775
  const summary = readSummary(value);
1395
- if (!isRecord(value) || value.state !== "open" && value.state !== "closed" || !Number.isSafeInteger(value.changed_files) || Number(value.changed_files) < 0) throw invalidResponse();
1396
- const author = isRecord(value.user) && typeof value.user.login === "string" ? value.user.login : void 0;
2776
+ if (!isRecord$2(value) || value.state !== "open" && value.state !== "closed" || !Number.isSafeInteger(value.changed_files) || Number(value.changed_files) < 0) throw invalidResponse();
2777
+ const author = isRecord$2(value.user) && typeof value.user.login === "string" ? value.user.login : void 0;
1397
2778
  const description = typeof value.body_text === "string" ? value.body_text : typeof value.body === "string" ? value.body : "";
1398
2779
  const state = value.merged_at ? "merged" : value.draft ? "draft" : value.state;
1399
2780
  return _objectSpread2(_objectSpread2({}, summary), {}, {
@@ -1406,7 +2787,7 @@ function readPullRequest(value) {
1406
2787
  });
1407
2788
  }
1408
2789
  function readFile(value) {
1409
- if (!isRecord(value) || typeof value.filename !== "string" || typeof value.status !== "string") throw invalidResponse();
2790
+ if (!isRecord$2(value) || typeof value.filename !== "string" || typeof value.status !== "string") throw invalidResponse();
1410
2791
  return {
1411
2792
  path: value.filename,
1412
2793
  previous: typeof value.previous_filename === "string" ? value.previous_filename : void 0,
@@ -1528,8 +2909,8 @@ function createGitHubPullRequests(repository, send = fetch) {
1528
2909
  let base;
1529
2910
  if (records.some((file) => file.path.split("/").pop() === "project.source.json" && file.status !== "added" && file.status !== "copied")) try {
1530
2911
  const comparison = await json(repo + "/compare/" + pr.base.sha + "..." + pr.head.sha + "?per_page=1", signal);
1531
- const merge = isRecord(comparison) ? comparison.merge_base_commit : null;
1532
- if (isRecord(merge) && isSha(merge.sha)) base = merge.sha;
2912
+ const merge = isRecord$2(comparison) ? comparison.merge_base_commit : null;
2913
+ if (isRecord$2(merge) && isSha(merge.sha)) base = merge.sha;
1533
2914
  } catch (error) {
1534
2915
  if (signal.aborted) throw error;
1535
2916
  }
@@ -1718,7 +3099,7 @@ function createScratchBlocksDiffLocator(resolveWorkspace) {
1718
3099
  if (!workspace || location.signal.aborted) return false;
1719
3100
  const block = workspace.getBlockById(location.id);
1720
3101
  const expected = location.target.blocks[location.id];
1721
- if (!block || isRecord(expected) && typeof expected.opcode === "string" && block.type !== expected.opcode) return false;
3102
+ if (!block || isRecord$2(expected) && typeof expected.opcode === "string" && block.type !== expected.opcode) return false;
1722
3103
  const clear = markBlockLocation(block, location.parts);
1723
3104
  if (!clear) return false;
1724
3105
  try {
@@ -1741,14 +3122,14 @@ function scriptRoot({ target, id }) {
1741
3122
  const visited = /* @__PURE__ */ new Set();
1742
3123
  for (;;) {
1743
3124
  const block = target.blocks[id];
1744
- if (!isRecord(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) return id;
3125
+ if (!isRecord$2(block) || typeof block.parent !== "string" || !Object.hasOwn(target.blocks, block.parent)) return id;
1745
3126
  if (visited.has(id)) throw new Error("Cyclic block parents.");
1746
3127
  visited.add(id);
1747
3128
  id = block.parent;
1748
3129
  }
1749
3130
  }
1750
3131
  function safeMutation(value, root, depth = 0) {
1751
- if (!isRecord(value) || depth > 32 || value.tagName !== (root ? "mutation" : "arg") || !Array.isArray(value.children)) return false;
3132
+ if (!isRecord$2(value) || depth > 32 || value.tagName !== (root ? "mutation" : "arg") || !Array.isArray(value.children)) return false;
1752
3133
  return Object.entries(value).every(([key, item]) => key === "children" || /^[a-zA-Z][\w-]*$/.test(key) && [
1753
3134
  "string",
1754
3135
  "number",
@@ -1773,7 +3154,7 @@ function selectBlocks({ target, id }, root, withContext) {
1773
3154
  ] : [...data];
1774
3155
  return;
1775
3156
  }
1776
- if (!isRecord(data) || typeof data.opcode !== "string") throw new Error("Invalid Scratch block.");
3157
+ if (!isRecord$2(data) || typeof data.opcode !== "string") throw new Error("Invalid Scratch block.");
1777
3158
  const block = structuredClone(data);
1778
3159
  if (block.mutation && !safeMutation(block.mutation, true)) throw new Error("Unsupported block mutation.");
1779
3160
  block.x = 20;
@@ -1784,7 +3165,7 @@ function selectBlocks({ target, id }, root, withContext) {
1784
3165
  if (!followNext) block.next = null;
1785
3166
  selected[blockId] = block;
1786
3167
  visiting.add(blockId);
1787
- if (isRecord(block.inputs)) for (const [name, input] of Object.entries(block.inputs)) {
3168
+ if (isRecord$2(block.inputs)) for (const [name, input] of Object.entries(block.inputs)) {
1788
3169
  if (!withContext && /^SUBSTACK\d*$/.test(name)) {
1789
3170
  delete block.inputs[name];
1790
3171
  continue;
@@ -2122,4 +3503,4 @@ function openProjectComparison(document, options) {
2122
3503
  render(/* @__PURE__ */ React.createElement(ProjectComparisonDialog, _objectSpread2(_objectSpread2({}, options), {}, { onChoose: choose })), root);
2123
3504
  });
2124
3505
  }
2125
- export { usePreviewSync as _, openProjectSourceTabs as a, useCostumeImage as b, gitHubRepositoryFromOrigin as c, assetFileName as d, organizeProjectChanges as f, createPreviewSync as g, BlockPreview as h, createScratchBlocksDiffLocator as i, ProjectSourceComparison as l, compareProjectForReview as m, openProjectComparison as n, PullRequestError as o, projectPropertyLabel as p, createScratchBlocksDiffRenderer as r, createGitHubPullRequests as s, ProjectComparisonDialog as t, ProjectSourceReview as u, currentCostume as v, CloseButton as x, useAssetImage as y };
3506
+ export { assetFileName as A, CloseButton as B, subscribeReviewDraft as C, GitHubReviewError as D, reviewLabelsFor as E, createPreviewSync as F, usePreviewSync as I, currentCostume as L, projectPropertyLabel as M, compareProjectForReview as N, createGitHubReviewApi as O, BlockPreview as P, useAssetImage as R, sendReview as S, labelsFor as T, probeReviewRoutes as _, openProjectSourceTabs as a, reviewProblem as b, gitHubRepositoryFromOrigin as c, PullRequestReviewComposer as d, checkReview as f, planReview as g, forgetReviewSubmission as h, createScratchBlocksDiffLocator as i, organizeProjectChanges as j, isReviewThreadPath as k, ProjectSourceComparison as l, emptyReviewDraft as m, openProjectComparison as n, PullRequestError as o, discardReview as p, createScratchBlocksDiffRenderer as r, createGitHubPullRequests as s, ProjectComparisonDialog as t, ProjectSourceReview as u, readReviewDraft as v, writeReviewDraft as w, reviewRoute as x, reviewBody as y, useCostumeImage as z };