@buzzni/saycode-cli 0.9.9 → 0.9.10
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/README.md +17 -1
- package/channel-extension-host.mjs +322 -20
- package/index.mjs +4835 -1525
- package/package.json +1 -1
- package/windows-host/saycode-windows-host.exe +0 -0
package/README.md
CHANGED
|
@@ -211,10 +211,20 @@ saycode를 따로 설치하지 않은 작업PC는 `npx @buzzni/saycode-cli searc
|
|
|
211
211
|
마무리합니다(specs/codexcli-approval-session-binding R12). 값이 없거나 모르는 값이면 아무것도 싣지 않고,
|
|
212
212
|
자식은 지금처럼 사람의 답을 기다립니다.
|
|
213
213
|
|
|
214
|
+
`agent prompt`의 결과는 `resultVersion:2`입니다. `ok:true`는 서버 저장 행을 다시 조회·복호화하여 본문과 설정까지 확인했다는 뜻이며 `storage.seq`가 확인한 위치입니다. `delivered:true`는 그 서버 메시지 ID의 **런타임 소비 증거**가 있어야 나옵니다. 구버전의 `delivered`는 같은 뜻으로 읽으면 안 됩니다. 큐 수락(receipt)만으로는 소비나 답변 성공이 아닙니다.
|
|
215
|
+
|
|
216
|
+
`--wait` 없이도 저장 뒤 runtime의 수락·거부 receipt를 최대 5초 기다려 거부를 바로 알려 주며, receipt가 없으면 `outcome:unconfirmed`로 돌아옵니다. `--wait`는 저장 이후 같은 입력 ID의 증인으로 기다립니다(최대 600초, 저장·검사 포함 620초). `response.attribution=bound`일 때만 `response.answer`가 이번 질문의 단독 답변이며, 공동 배치·steer·질문/자유문 권한 답변이 끼면 `consumed-with`와 입력 목록/사유를 돌려줍니다. receipt timeout은 `outcome:unconfirmed`이고 실패로 확정하지 않습니다. 소비는 확인됐지만 마감까지 작업이 끝나지 않으면 같은 증거를 담은 `wait_timeout`(exit 3)이고, 소비된 턴이 `--until` 조건 없이 끝나면(예: `consumed-with`) 마감을 기다리지 않고 `outcome:unconfirmed`로 바로 돌려줍니다. `next.command` 또는 `happy agent read <id> --since <storage.seq>`로 늦은 receipt·소비를 정산하세요.
|
|
217
|
+
|
|
218
|
+
재실행에 대비하려면 **송신 전** `--local-id <id>`와 대상·본문·설정을 보관합니다(1–128 ASCII 영숫자 및 `._:-`). 같은 ID/동일 intent로 저장 응답 유실을 한 번만 재시도하며, 다른 본문이나 설정이면 `localid_conflict`입니다. CLI가 죽은 뒤에는 보관한 같은 ID로 다시 실행해 원래 저장 행을 확인합니다. 그사이 대상 runtime이 재시작되면 확인 조건이 달라져 `localid_conflict`가 되지만, 본문이 같으면(`storage.existing.sameBody:true`) `storage.existing`과 `next.command`(`happy agent read <id> --since <seq>`)로 원래 요청의 소비 여부를 확인할 수 있습니다. 본문이 다르면 그 행은 다른 질문이므로 `next.command`를 주지 않습니다. ID를 보관하지 않은 송신은 본문·시각으로 찾지 않으므로 결과를 알 수 없습니다. 복호화/조회 실패는 `storage-verification-unknown`입니다. 미확인 요청을 새 ID로 자동 재전송하지 않습니다.
|
|
219
|
+
|
|
220
|
+
입력 증거는 지원 Claude/Codex 세션의 명시적 opt-in(`HAPPY_INPUT_EVIDENCE=1`, 기본 OFF)과 살아 있는 동일 runtime/remote epoch 확인이 필요합니다. 그 확인이 안 되는 세션(ON marker 없는 .358, 미지원·OFF 세션)은 `CLI_GUARDED_PROMPT_ROLLOUT`(기본 off) 동안 이전처럼 보내되 결과는 `resultVersion:2`, `delivered:false`, `delivery.state:"unsupported"`로 소비를 주장하지 않습니다. 이 플래그를 켜면 같은 세션은 `prompt_unsupported`로 보내기 전에 닫힙니다. Desktop 자동 opt-in은 T2r pin 전까지 꺼져 있습니다. A6 생성 안내는 T4rt와 함께 수락합니다. 허브 자동 알림은 `HUB_BRIDGE_OWNERSHIP_ROLLOUT`(기본 off) 동안 이전과 같고, 켜면 main이 개인은 같은 계정, 회사는 confirmed owner만 수신하도록 확인하며 확인 실패 시 배지에서 복구를 안내합니다. 사람의 직접 입력은 유지됩니다.
|
|
221
|
+
|
|
214
222
|
`steer` 성공의 `accepted:true`는 세션 RPC가 요청을 받았다는 뜻이며 실제 transcript 전달 완료를
|
|
215
223
|
뜻하지 않습니다(`delivered:false`). 끝난 턴의 후속 작업에는 `prompt`를 사용합니다. 에이전트 세션은
|
|
216
224
|
자기 자신에게 입력할 수 없고, `steer`와 `stop`은 depth와 관계없이 원장에 기록된 자기 후손에게만 보낼 수
|
|
217
|
-
있습니다.
|
|
225
|
+
있습니다. 예외로 사람이 시작한 Claude·Codex 허브(broker 경로, depth 0)는 소유가 검증된 형제 세션(`ls`의
|
|
226
|
+
`ownership.relation:"own-sibling"`)에도 보낼 수 있으며, 이때 `stop`은 프로세스를 끄지 않고 지금 응답만 끊습니다
|
|
227
|
+
(`mode:"turn-abort"`, 터미널에서 쓰는 Claude는 stop을 `controlled-by-user`, steer를 `local-mode`로 거부). 형제 결과에는 대상 `title`과 `ownership.relation`이 붙습니다. 형제에게 보내기 전에 대상 제목과 전체 ID를 확인합니다. 후손이 아닌 세션에 `prompt`하려면 소유가 검증돼야 하며, `not_owned`를 받으면 `read`로만 보고
|
|
218
228
|
다시 시도하지 않습니다(ADR-078). `stop`은 보관·삭제를 하지 않습니다. spawn 한도(`whoami`의 `maxSpawn`)는 세션이
|
|
219
229
|
도는 머신의 코어·메모리에서 산정하므로 머신마다 다르고, 최근 6시간 안의 spawn만 세는 롤링 윈도우라
|
|
220
230
|
소진돼도 시간이 지나면 회복됩니다(`budgetResetsAt`, ADR-063). 데몬이 확인하지 못한 시도도 같은
|
|
@@ -510,3 +520,9 @@ node ../../scripts/verify-cli-tarball.mjs out/cli/buzzni-saycode-cli-*.tgz
|
|
|
510
520
|
|
|
511
521
|
번들에 포함된 서드파티 오픈소스는 각자의 라이선스를 유지하며, 목록과 전문은 패키지에
|
|
512
522
|
동봉된 `THIRD-PARTY-NOTICES.md`에 있습니다 (빌드 시 실제 번들 내용에서 자동 생성).
|
|
523
|
+
|
|
524
|
+
T2 응답은 소비한 native turn과 runtime에 결속합니다. `prompt --wait`의 질문·구조화 출력도 그 턴만 판정하며 `structuredOutput`과 `structured_output_missing`을 v2에 유지합니다. 명시적 runtime 거부는 `prompt_rejected`, exit 3이며 localId·storage·거부 증거를 보존합니다. 백그라운드 후속 관측은 `response.followUps`에 따로 남기고 최초 답변을 덮지 않습니다.
|
|
525
|
+
|
|
526
|
+
송신 당시 검증된 runtime/epoch는 암호화 요청에 보존하므로 host 재시작 뒤에도 `read --since`로 과거 소비 사실을 조회할 수 있습니다. 이 기록은 새 runtime에 송신 권한을 주지 않습니다. 검증 기록이 없는 구 요청은 현재 runtime이 같고 ON이어도 unknown으로 남습니다. 기록을 소급 생성하거나 과거 소비를 확인됐다고 표시하지 않습니다.
|
|
527
|
+
|
|
528
|
+
Codex 명령/패치 승인은 tool 시작 전에 대기할 수 있지만 .359의 REST pending 상태에는 approval↔turn 결속이 없습니다. `prompt --wait --until waiting-input`은 이 승인을 현재 턴으로 추측하지 않습니다. 해당 대기가 확인되지 않은 채 timeout되면 `outcome:unconfirmed`, `waitingInput:{state:"unsupported",reason:"pending-approval-turn-binding-unavailable"}`를 반환합니다. 이는 pending 승인이 요청 B에 속한다는 뜻이 아닙니다. Claude 질문·Codex 질문은 실제 tool-call ID/턴 결속이 있을 때 판정하며, 채널 routing requestId로 대체하지 않습니다. 미확인 결과를 새 메시지로 자동 재시도하지 마세요.
|
|
@@ -7,7 +7,7 @@ const ASSET_PERMISSIONS = ["assets.read", "assets.write", "files.select", "draft
|
|
|
7
7
|
function usesAssetWorkflow(permissions) {
|
|
8
8
|
return permissions.some((permission) => ASSET_PERMISSIONS.includes(permission));
|
|
9
9
|
}
|
|
10
|
-
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "machine.run"];
|
|
10
|
+
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup", "machine.run"];
|
|
11
11
|
function extensionRequestTimeoutMs(permissions, side) {
|
|
12
12
|
const long = usesAssetWorkflow(permissions) || permissions.some((permission) => LONG_RUNNING_PERMISSIONS.includes(permission));
|
|
13
13
|
if (!long) return void 0;
|
|
@@ -469,8 +469,11 @@ function parseExtensionArtifactActions(value, path2 = "manifest.contributes.arti
|
|
|
469
469
|
};
|
|
470
470
|
});
|
|
471
471
|
}
|
|
472
|
-
const CHANNEL_API_VERSION =
|
|
472
|
+
const CHANNEL_API_VERSION = 2;
|
|
473
473
|
const CHANNEL_MINIMUM_API_VERSION = 1;
|
|
474
|
+
function isSupportedChannelApiVersion(value) {
|
|
475
|
+
return typeof value === "number" && Number.isInteger(value) && value >= CHANNEL_MINIMUM_API_VERSION && value <= CHANNEL_API_VERSION;
|
|
476
|
+
}
|
|
474
477
|
const CHANNEL_PROVIDERS = ["telegram", "slack", "discord"];
|
|
475
478
|
const CHANNEL_COMMAND_SCHEMA = Object.freeze({
|
|
476
479
|
pair: Object.freeze(["code!"]),
|
|
@@ -611,6 +614,7 @@ const EXTENSION_PERMISSIONS = [
|
|
|
611
614
|
"localTools.install",
|
|
612
615
|
"localTools.control",
|
|
613
616
|
"localBrowser.setup",
|
|
617
|
+
"browser.runtimeSetup",
|
|
614
618
|
"network.fetch",
|
|
615
619
|
"notifications.show",
|
|
616
620
|
"storage.read",
|
|
@@ -633,7 +637,11 @@ const EXTENSION_PERMISSIONS = [
|
|
|
633
637
|
"channels.transport",
|
|
634
638
|
"sessions.read",
|
|
635
639
|
"sessions.control",
|
|
636
|
-
"sessions.approveOnce"
|
|
640
|
+
"sessions.approveOnce",
|
|
641
|
+
// Core → adapter, not a capability the adapter invokes: Core sends the current input and the
|
|
642
|
+
// thread's earlier messages (other people's words included) for display-text normalization
|
|
643
|
+
// (specs/slack-bot-improvements contracts §2). Channel API 2 only.
|
|
644
|
+
"channels.normalize"
|
|
637
645
|
];
|
|
638
646
|
const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefined" && __SAYCODE_MODS_INTERNAL__ === true;
|
|
639
647
|
const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
|
|
@@ -644,7 +652,7 @@ const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
|
|
|
644
652
|
const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
|
|
645
653
|
class UnsupportedChannelApiVersionError extends Error {
|
|
646
654
|
constructor(channelApiVersion) {
|
|
647
|
-
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${
|
|
655
|
+
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${`${SAYCODE_CHANNEL_MINIMUM_API_VERSION}-${SAYCODE_CHANNEL_API_VERSION}`}`);
|
|
648
656
|
this.channelApiVersion = channelApiVersion;
|
|
649
657
|
}
|
|
650
658
|
name = "UnsupportedChannelApiVersionError";
|
|
@@ -667,7 +675,7 @@ const V2_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
667
675
|
"browserViewer.install",
|
|
668
676
|
"browserViewer.installChrome"
|
|
669
677
|
]);
|
|
670
|
-
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup"]);
|
|
678
|
+
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup"]);
|
|
671
679
|
const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
672
680
|
"channels.receive",
|
|
673
681
|
"channels.ack",
|
|
@@ -675,7 +683,8 @@ const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
675
683
|
"channels.transport",
|
|
676
684
|
"sessions.read",
|
|
677
685
|
"sessions.control",
|
|
678
|
-
"sessions.approveOnce"
|
|
686
|
+
"sessions.approveOnce",
|
|
687
|
+
"channels.normalize"
|
|
679
688
|
]);
|
|
680
689
|
const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
681
690
|
"id",
|
|
@@ -925,6 +934,9 @@ function parseExtensionManifest(value, options) {
|
|
|
925
934
|
if (channelApiVersion === void 0) {
|
|
926
935
|
fail$5(`manifest.permissions[${index}]`, "requires manifest.channelApiVersion");
|
|
927
936
|
}
|
|
937
|
+
if (typed === "channels.normalize" && channelApiVersion < 2) {
|
|
938
|
+
fail$5(`manifest.permissions[${index}]`, "channels.normalize requires channel API version 2");
|
|
939
|
+
}
|
|
928
940
|
}
|
|
929
941
|
return typed;
|
|
930
942
|
});
|
|
@@ -1073,6 +1085,246 @@ function parseChannelApprovalRequest(value) {
|
|
|
1073
1085
|
}
|
|
1074
1086
|
return { approvalHandle, approveLabel, denyLabel };
|
|
1075
1087
|
}
|
|
1088
|
+
const NORMALIZE_ADDITION_SOURCES = ["blocks", "quote", "forward", "table", "unfurl", "files"];
|
|
1089
|
+
const MAX_NORMALIZE_ITEMS = 200;
|
|
1090
|
+
const MAX_USER_LABELS = 200;
|
|
1091
|
+
const MAX_BLOCKS = 50;
|
|
1092
|
+
const MAX_ATTACHMENTS = 20;
|
|
1093
|
+
const MAX_FILES = 20;
|
|
1094
|
+
const MAX_RICH_TEXT_DEPTH = 6;
|
|
1095
|
+
const MAX_URL_LENGTH = 2048;
|
|
1096
|
+
const MAX_ADDITIONS_BYTES = 8192;
|
|
1097
|
+
const MAX_PRIMARY_BYTES = 65536;
|
|
1098
|
+
const ITEM_REF = /^i(0|[1-9]\d?|1\d\d)$/;
|
|
1099
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1100
|
+
const isString = (value) => typeof value === "string";
|
|
1101
|
+
function projectSlackMessage(raw) {
|
|
1102
|
+
const state = { truncated: false };
|
|
1103
|
+
const message = isRecord(raw) ? raw : {};
|
|
1104
|
+
const projection = {};
|
|
1105
|
+
const blocks = projectBlocks(message.blocks, state);
|
|
1106
|
+
if (blocks) projection.blocks = blocks;
|
|
1107
|
+
const attachments = capped(Array.isArray(message.attachments) ? message.attachments.filter(isRecord) : [], MAX_ATTACHMENTS, state).map((attachment) => projectAttachment(attachment, state));
|
|
1108
|
+
if (attachments.length > 0) projection.attachments = attachments;
|
|
1109
|
+
const files = capped(Array.isArray(message.files) ? message.files.filter(isRecord) : [], MAX_FILES, state).map(projectFile);
|
|
1110
|
+
if (files.length > 0) projection.files = files;
|
|
1111
|
+
if (isString(message.subtype)) projection.subtype = message.subtype;
|
|
1112
|
+
return { projection, truncated: state.truncated };
|
|
1113
|
+
}
|
|
1114
|
+
function capped(items, max, state) {
|
|
1115
|
+
if (items.length > max) state.truncated = true;
|
|
1116
|
+
return items.slice(0, max);
|
|
1117
|
+
}
|
|
1118
|
+
function projectBlocks(value, state) {
|
|
1119
|
+
if (!Array.isArray(value)) return void 0;
|
|
1120
|
+
const projected = value.map((block) => projectBlock(block, state)).filter((block) => block !== null);
|
|
1121
|
+
return projected.length > 0 ? capped(projected, MAX_BLOCKS, state) : void 0;
|
|
1122
|
+
}
|
|
1123
|
+
function projectBlock(block, state) {
|
|
1124
|
+
if (!isRecord(block)) return null;
|
|
1125
|
+
switch (block.type) {
|
|
1126
|
+
case "rich_text":
|
|
1127
|
+
return { type: "rich_text", elements: projectContainers(block.elements, 1, state) };
|
|
1128
|
+
case "section": {
|
|
1129
|
+
const section = { type: "section" };
|
|
1130
|
+
const text2 = projectTextObject(block.text);
|
|
1131
|
+
if (text2) section.text = text2;
|
|
1132
|
+
if (Array.isArray(block.fields)) section.fields = block.fields.map(projectTextObject).filter((field) => field !== null);
|
|
1133
|
+
return section;
|
|
1134
|
+
}
|
|
1135
|
+
case "header": {
|
|
1136
|
+
const text2 = projectTextObject(block.text);
|
|
1137
|
+
return text2 ? { type: "header", text: text2 } : null;
|
|
1138
|
+
}
|
|
1139
|
+
case "context":
|
|
1140
|
+
return { type: "context", elements: Array.isArray(block.elements) ? block.elements.map(projectTextObject).filter((element) => element !== null) : [] };
|
|
1141
|
+
case "table":
|
|
1142
|
+
return { type: "table", rows: Array.isArray(block.rows) ? block.rows.filter(Array.isArray).map((row2) => row2.flatMap((cell) => {
|
|
1143
|
+
if (isRecord(cell) && cell.type === "raw_text" && isString(cell.text)) return [{ type: "raw_text", text: cell.text }];
|
|
1144
|
+
if (isRecord(cell) && cell.type === "rich_text") return [{ type: "rich_text", elements: projectContainers(cell.elements, 1, state) }];
|
|
1145
|
+
return [];
|
|
1146
|
+
})) : [] };
|
|
1147
|
+
default:
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function projectTextObject(value) {
|
|
1152
|
+
if (!isRecord(value) || value.type !== "mrkdwn" && value.type !== "plain_text" || !isString(value.text)) return null;
|
|
1153
|
+
return { type: value.type, text: value.text };
|
|
1154
|
+
}
|
|
1155
|
+
const CONTAINER_TYPES = /* @__PURE__ */ new Set(["rich_text_section", "rich_text_quote", "rich_text_preformatted", "rich_text_list"]);
|
|
1156
|
+
function projectContainers(value, depth, state) {
|
|
1157
|
+
if (!Array.isArray(value)) return [];
|
|
1158
|
+
return value.flatMap((node) => {
|
|
1159
|
+
const container = isRecord(node) && CONTAINER_TYPES.has(String(node.type)) ? projectContainer(node, depth, state) : null;
|
|
1160
|
+
return container ? [container] : [];
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
function projectContainer(node, depth, state) {
|
|
1164
|
+
if (depth > MAX_RICH_TEXT_DEPTH) {
|
|
1165
|
+
state.truncated = true;
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const elements = (Array.isArray(node.elements) ? node.elements : []).flatMap((child) => {
|
|
1169
|
+
if (isRecord(child) && CONTAINER_TYPES.has(String(child.type))) {
|
|
1170
|
+
const nested = projectContainer(child, depth + 1, state);
|
|
1171
|
+
return nested ? [nested] : [];
|
|
1172
|
+
}
|
|
1173
|
+
const leaf = projectLeaf(child);
|
|
1174
|
+
return leaf ? [leaf] : [];
|
|
1175
|
+
});
|
|
1176
|
+
const container = { type: node.type, elements };
|
|
1177
|
+
if (node.style === "bullet" || node.style === "ordered") container.style = node.style;
|
|
1178
|
+
return container;
|
|
1179
|
+
}
|
|
1180
|
+
function projectLeaf(node) {
|
|
1181
|
+
if (!isRecord(node)) return null;
|
|
1182
|
+
switch (node.type) {
|
|
1183
|
+
case "text": {
|
|
1184
|
+
if (!isString(node.text)) return null;
|
|
1185
|
+
const leaf = { type: "text", text: node.text };
|
|
1186
|
+
if (isRecord(node.style)) {
|
|
1187
|
+
const style = Object.fromEntries(["bold", "italic", "strike", "code"].filter((key) => typeof node.style[key] === "boolean").map((key) => [key, node.style[key]]));
|
|
1188
|
+
if (Object.keys(style).length > 0) leaf.style = style;
|
|
1189
|
+
}
|
|
1190
|
+
return leaf;
|
|
1191
|
+
}
|
|
1192
|
+
case "link": {
|
|
1193
|
+
const label = isString(node.text) ? node.text : void 0;
|
|
1194
|
+
if (!httpUrl(node.url)) return label === void 0 ? null : { type: "text", text: label };
|
|
1195
|
+
return label === void 0 ? { type: "link", url: node.url } : { type: "link", url: node.url, text: label };
|
|
1196
|
+
}
|
|
1197
|
+
case "user":
|
|
1198
|
+
return isString(node.user_id) ? { type: "user", user_id: node.user_id } : null;
|
|
1199
|
+
case "channel":
|
|
1200
|
+
return isString(node.channel_id) ? { type: "channel", channel_id: node.channel_id } : null;
|
|
1201
|
+
case "usergroup":
|
|
1202
|
+
return isString(node.usergroup_id) ? { type: "usergroup", usergroup_id: node.usergroup_id } : null;
|
|
1203
|
+
case "emoji":
|
|
1204
|
+
return isString(node.name) ? { type: "emoji", name: node.name } : null;
|
|
1205
|
+
case "broadcast":
|
|
1206
|
+
return node.range === "here" || node.range === "channel" || node.range === "everyone" ? { type: "broadcast", range: node.range } : null;
|
|
1207
|
+
default:
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const ATTACHMENT_TEXT_KEYS = ["text", "fallback", "pretext", "title", "author_name", "footer"];
|
|
1212
|
+
const ATTACHMENT_URL_KEYS = ["title_link", "from_url", "original_url"];
|
|
1213
|
+
function projectAttachment(attachment, state) {
|
|
1214
|
+
const projected = {};
|
|
1215
|
+
for (const key of ATTACHMENT_TEXT_KEYS) if (isString(attachment[key])) projected[key] = attachment[key];
|
|
1216
|
+
for (const key of ATTACHMENT_URL_KEYS) if (httpUrl(attachment[key])) projected[key] = attachment[key];
|
|
1217
|
+
if (typeof attachment.is_share === "boolean") projected.is_share = attachment.is_share;
|
|
1218
|
+
if (typeof attachment.is_msg_unfurl === "boolean") projected.is_msg_unfurl = attachment.is_msg_unfurl;
|
|
1219
|
+
if (Array.isArray(attachment.message_blocks)) {
|
|
1220
|
+
projected.message_blocks = attachment.message_blocks.filter(isRecord).map((forwarded) => ({
|
|
1221
|
+
message: { blocks: projectBlocks(isRecord(forwarded.message) ? forwarded.message.blocks : void 0, state) ?? [] }
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
return projected;
|
|
1225
|
+
}
|
|
1226
|
+
function projectFile(file) {
|
|
1227
|
+
const projected = {};
|
|
1228
|
+
for (const key of ["name", "title", "filetype", "mimetype"]) if (isString(file[key])) projected[key] = file[key];
|
|
1229
|
+
if (Number.isSafeInteger(file.size) && file.size >= 0) projected.size = file.size;
|
|
1230
|
+
return projected;
|
|
1231
|
+
}
|
|
1232
|
+
function httpUrl(value) {
|
|
1233
|
+
return isString(value) && value.length <= MAX_URL_LENGTH && /^https?:\/\//i.test(value);
|
|
1234
|
+
}
|
|
1235
|
+
function parseChannelNormalizeRequest(value) {
|
|
1236
|
+
const request = plainRecord(value, "normalize request");
|
|
1237
|
+
exactKeySet(request, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision", "requestId", "purpose", "userLabels", "items"], "normalize request");
|
|
1238
|
+
if (request.apiVersion !== 2 || request.provider !== "slack") fail("normalize request is channel API 2 Slack only");
|
|
1239
|
+
if (request.purpose !== "currentInput" && request.purpose !== "threadHistory") fail("invalid normalize purpose");
|
|
1240
|
+
if (!Number.isSafeInteger(request.connectionRevision) || request.connectionRevision < 1) fail("invalid normalize connection revision");
|
|
1241
|
+
const labels = plainRecord(request.userLabels, "normalize userLabels");
|
|
1242
|
+
if (Object.keys(labels).length > MAX_USER_LABELS || Object.entries(labels).some(([key, label]) => key.length === 0 || key.length > 64 || !isString(label) || label.length > 256)) {
|
|
1243
|
+
fail("invalid normalize userLabels");
|
|
1244
|
+
}
|
|
1245
|
+
if (!Array.isArray(request.items) || request.items.length < 1 || request.items.length > MAX_NORMALIZE_ITEMS) fail("invalid normalize items");
|
|
1246
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1247
|
+
const items = request.items.map((value2) => {
|
|
1248
|
+
const item = plainRecord(value2, "normalize item");
|
|
1249
|
+
exactKeySet(item, ["itemRef", "primaryText", "projection", "maxAdditionsBytes"], "normalize item");
|
|
1250
|
+
if (!isString(item.itemRef) || !ITEM_REF.test(item.itemRef) || refs.has(item.itemRef)) fail("invalid normalize itemRef");
|
|
1251
|
+
refs.add(item.itemRef);
|
|
1252
|
+
if (!isString(item.primaryText) || utf8Bytes(item.primaryText) > MAX_PRIMARY_BYTES) fail("invalid normalize primaryText");
|
|
1253
|
+
if (!Number.isSafeInteger(item.maxAdditionsBytes) || item.maxAdditionsBytes < 1 || item.maxAdditionsBytes > MAX_ADDITIONS_BYTES) fail("invalid normalize maxAdditionsBytes");
|
|
1254
|
+
const projected = projectSlackMessage(item.projection);
|
|
1255
|
+
if (!isRecord(item.projection) || projected.truncated || !sameJson(projected.projection, item.projection)) fail("normalize projection is not a Core projection");
|
|
1256
|
+
return { itemRef: item.itemRef, primaryText: item.primaryText, projection: projected.projection, maxAdditionsBytes: item.maxAdditionsBytes };
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
apiVersion: 2,
|
|
1260
|
+
provider: "slack",
|
|
1261
|
+
purpose: request.purpose,
|
|
1262
|
+
channelId: boundedText(request.channelId, "channelId"),
|
|
1263
|
+
connectionId: boundedText(request.connectionId, "connectionId"),
|
|
1264
|
+
connectionRevision: request.connectionRevision,
|
|
1265
|
+
requestId: boundedText(request.requestId, "requestId"),
|
|
1266
|
+
userLabels: { ...labels },
|
|
1267
|
+
items
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function parseChannelNormalizeResult(request, value) {
|
|
1271
|
+
const result = plainRecord(value, "normalize result");
|
|
1272
|
+
exactKeySet(result, ["items"], "normalize result");
|
|
1273
|
+
if (!Array.isArray(result.items)) fail("invalid normalize result items");
|
|
1274
|
+
const wanted = new Map(request.items.map((item) => [item.itemRef, item]));
|
|
1275
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const value2 of result.items) {
|
|
1277
|
+
const item = plainRecord(value2, "normalize result item");
|
|
1278
|
+
exactKeySet(item, ["itemRef", "additions", "truncated"], "normalize result item");
|
|
1279
|
+
const requested = isString(item.itemRef) ? wanted.get(item.itemRef) : void 0;
|
|
1280
|
+
if (!requested || parsed.has(requested.itemRef)) fail("normalize result items differ from the request");
|
|
1281
|
+
if (typeof item.truncated !== "boolean" || !Array.isArray(item.additions)) fail("invalid normalize result item");
|
|
1282
|
+
let bytes = 0;
|
|
1283
|
+
const additions = item.additions.map((value3) => {
|
|
1284
|
+
const addition = plainRecord(value3, "normalize addition");
|
|
1285
|
+
exactKeySet(addition, ["source", "text"], "normalize addition");
|
|
1286
|
+
if (!NORMALIZE_ADDITION_SOURCES.includes(addition.source) || !isString(addition.text) || addition.text.includes("\0") || hasLoneSurrogate(addition.text)) fail("invalid normalize addition");
|
|
1287
|
+
bytes += utf8Bytes(addition.text);
|
|
1288
|
+
return { source: addition.source, text: addition.text };
|
|
1289
|
+
});
|
|
1290
|
+
if (bytes > requested.maxAdditionsBytes) fail("normalize additions exceed their budget");
|
|
1291
|
+
parsed.set(requested.itemRef, { itemRef: requested.itemRef, additions: withoutRepeatedLines(requested.primaryText, additions), truncated: item.truncated });
|
|
1292
|
+
}
|
|
1293
|
+
if (parsed.size !== wanted.size) fail("normalize result items differ from the request");
|
|
1294
|
+
return request.items.map((item) => parsed.get(item.itemRef));
|
|
1295
|
+
}
|
|
1296
|
+
function withoutRepeatedLines(primaryText, additions) {
|
|
1297
|
+
const fold = (line) => line.replace(/\s+/gu, " ").trim();
|
|
1298
|
+
const known = new Set(primaryText.split("\n").map(fold).filter(Boolean));
|
|
1299
|
+
const kept = [];
|
|
1300
|
+
for (const addition of additions) {
|
|
1301
|
+
const lines2 = addition.text.split("\n");
|
|
1302
|
+
const remaining = lines2.filter((line) => fold(line) === "" || !known.has(fold(line)));
|
|
1303
|
+
for (const line of lines2) if (fold(line)) known.add(fold(line));
|
|
1304
|
+
if (remaining.some((line) => fold(line) !== "")) kept.push({ source: addition.source, text: remaining.join("\n") });
|
|
1305
|
+
}
|
|
1306
|
+
return kept;
|
|
1307
|
+
}
|
|
1308
|
+
function exactKeySet(record2, keys2, path2) {
|
|
1309
|
+
const actual = Object.keys(record2);
|
|
1310
|
+
if (actual.length !== keys2.length || !keys2.every((key) => actual.includes(key))) fail(`${path2} has unexpected keys`);
|
|
1311
|
+
}
|
|
1312
|
+
function boundedText(value, path2) {
|
|
1313
|
+
if (!isString(value) || value.length === 0 || value.length > 200) fail(`invalid normalize ${path2}`);
|
|
1314
|
+
return value;
|
|
1315
|
+
}
|
|
1316
|
+
function sameJson(a, b) {
|
|
1317
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1318
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
|
|
1319
|
+
}
|
|
1320
|
+
if (isRecord(a) && isRecord(b)) {
|
|
1321
|
+
const keys2 = Object.keys(a);
|
|
1322
|
+
return keys2.length === Object.keys(b).length && keys2.every((key) => Object.hasOwn(b, key) && sameJson(a[key], b[key]));
|
|
1323
|
+
}
|
|
1324
|
+
return Object.is(a, b);
|
|
1325
|
+
}
|
|
1326
|
+
const utf8Bytes = (text2) => new TextEncoder().encode(text2).length;
|
|
1327
|
+
const hasLoneSurrogate = (text2) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text2);
|
|
1076
1328
|
function finiteNumber(value, path2) {
|
|
1077
1329
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${path2} must be a finite number`);
|
|
1078
1330
|
return value;
|
|
@@ -1162,13 +1414,15 @@ function parseRequest(message) {
|
|
|
1162
1414
|
"maxChunkUtf16Units",
|
|
1163
1415
|
"maxChunks"
|
|
1164
1416
|
];
|
|
1417
|
+
const v2 = params.apiVersion === 2;
|
|
1165
1418
|
exactKeys(
|
|
1166
1419
|
params,
|
|
1167
|
-
"approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1420
|
+
v2 ? [...formatKeys, "mode", "dialect"] : "approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1168
1421
|
"request.params"
|
|
1169
1422
|
);
|
|
1423
|
+
if (v2 && (params.mode !== "verbatim" && params.mode !== "rendered" || params.dialect !== "slack-mrkdwn" || params.kind === "approval")) fail("invalid channel API 2 formatter request");
|
|
1170
1424
|
const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: {
|
|
1171
|
-
apiVersion: params.apiVersion,
|
|
1425
|
+
apiVersion: v2 ? 1 : params.apiVersion,
|
|
1172
1426
|
channelId: params.channelId,
|
|
1173
1427
|
connectionId: params.connectionId,
|
|
1174
1428
|
provider: params.provider,
|
|
@@ -1184,16 +1438,26 @@ function parseRequest(message) {
|
|
|
1184
1438
|
}
|
|
1185
1439
|
const approval = params.approval === void 0 ? void 0 : parseChannelApprovalRequest(params.approval);
|
|
1186
1440
|
const { state: _state, ...identity } = lifecycle.params;
|
|
1441
|
+
const bounds = { requestId, text: body, maxChunks: Number(params.maxChunks), maxChunkUtf16Units: Number(params.maxChunkUtf16Units) };
|
|
1442
|
+
if (v2) {
|
|
1443
|
+
return { kind: "request", id: id2, method, params: {
|
|
1444
|
+
...identity,
|
|
1445
|
+
...bounds,
|
|
1446
|
+
apiVersion: 2,
|
|
1447
|
+
kind: params.kind,
|
|
1448
|
+
mode: params.mode,
|
|
1449
|
+
dialect: "slack-mrkdwn"
|
|
1450
|
+
} };
|
|
1451
|
+
}
|
|
1187
1452
|
return { kind: "request", id: id2, method, params: {
|
|
1188
1453
|
...identity,
|
|
1189
|
-
|
|
1190
|
-
text: body,
|
|
1454
|
+
...bounds,
|
|
1191
1455
|
kind: params.kind,
|
|
1192
|
-
maxChunks: Number(params.maxChunks),
|
|
1193
|
-
maxChunkUtf16Units: Number(params.maxChunkUtf16Units),
|
|
1194
1456
|
...approval ? { approval } : {}
|
|
1195
1457
|
} };
|
|
1196
1458
|
}
|
|
1459
|
+
case "channel.normalizeMessages":
|
|
1460
|
+
return { kind: "request", id: id2, method, params: parseChannelNormalizeRequest(params) };
|
|
1197
1461
|
case "invokeCommand": {
|
|
1198
1462
|
exactKeys(params, ["commandId", "args"], "request.params");
|
|
1199
1463
|
if (!Array.isArray(params.args)) fail("request.params.args must be an array");
|
|
@@ -1276,6 +1540,17 @@ function parseExtensionWireMessage(value) {
|
|
|
1276
1540
|
if (message.kind === "event") return parseEvent(message);
|
|
1277
1541
|
fail("message.kind is unknown");
|
|
1278
1542
|
}
|
|
1543
|
+
function parseChannelRenderedReplyResult(value) {
|
|
1544
|
+
const result = plainRecord(value, "channel rendered result");
|
|
1545
|
+
if (Object.keys(result).join() !== "rendered" || !Array.isArray(result.rendered)) fail("invalid channel rendered result");
|
|
1546
|
+
return {
|
|
1547
|
+
rendered: result.rendered.map((value2) => {
|
|
1548
|
+
const chunk = plainRecord(value2, "channel rendered chunk");
|
|
1549
|
+
if (Object.keys(chunk).sort().join() !== "sourceEnd,sourceStart,text" || typeof chunk.text !== "string" || !Number.isSafeInteger(chunk.sourceStart) || !Number.isSafeInteger(chunk.sourceEnd)) fail("invalid channel rendered chunk");
|
|
1550
|
+
return { text: chunk.text, sourceStart: chunk.sourceStart, sourceEnd: chunk.sourceEnd };
|
|
1551
|
+
})
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1279
1554
|
const TIMEOUT = Symbol("extension-request-timeout");
|
|
1280
1555
|
function requestEnvelope(value) {
|
|
1281
1556
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -1297,6 +1572,7 @@ class ExtensionHostRuntime {
|
|
|
1297
1572
|
widgets = /* @__PURE__ */ new Map();
|
|
1298
1573
|
channelHandlers = /* @__PURE__ */ new Map();
|
|
1299
1574
|
channelFormatters = /* @__PURE__ */ new Map();
|
|
1575
|
+
channelNormalizers = /* @__PURE__ */ new Map();
|
|
1300
1576
|
channelIntakeHandlers = /* @__PURE__ */ new Map();
|
|
1301
1577
|
channelStates = /* @__PURE__ */ new Map();
|
|
1302
1578
|
channelCalls = /* @__PURE__ */ new Map();
|
|
@@ -1432,6 +1708,7 @@ class ExtensionHostRuntime {
|
|
|
1432
1708
|
this.commandHandlers.clear();
|
|
1433
1709
|
this.channelHandlers.clear();
|
|
1434
1710
|
this.channelFormatters.clear();
|
|
1711
|
+
this.channelNormalizers.clear();
|
|
1435
1712
|
this.channelIntakeHandlers.clear();
|
|
1436
1713
|
this.channelStates.clear();
|
|
1437
1714
|
this.channelCalls.clear();
|
|
@@ -1475,6 +1752,7 @@ class ExtensionHostRuntime {
|
|
|
1475
1752
|
this.commandHandlers.clear();
|
|
1476
1753
|
this.channelHandlers.clear();
|
|
1477
1754
|
this.channelFormatters.clear();
|
|
1755
|
+
this.channelNormalizers.clear();
|
|
1478
1756
|
this.channelIntakeHandlers.clear();
|
|
1479
1757
|
this.channelStates.clear();
|
|
1480
1758
|
this.channelCalls.clear();
|
|
@@ -1495,6 +1773,7 @@ class ExtensionHostRuntime {
|
|
|
1495
1773
|
}
|
|
1496
1774
|
if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
|
|
1497
1775
|
if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
|
|
1776
|
+
if (request.method === "channel.normalizeMessages") return this.normalizeChannelMessages(request.params);
|
|
1498
1777
|
if (request.method === "channel.intake") return this.deliverChannelIntake(request.params);
|
|
1499
1778
|
const handler = this.commandHandlers.get(request.params.commandId);
|
|
1500
1779
|
if (!handler) {
|
|
@@ -1507,7 +1786,7 @@ class ExtensionHostRuntime {
|
|
|
1507
1786
|
}
|
|
1508
1787
|
async deliverChannel(event) {
|
|
1509
1788
|
const contribution = this.options.manifest.contributes.channels?.find((channel) => channel.id === event.channelId);
|
|
1510
|
-
if (this.options.manifest.channelApiVersion
|
|
1789
|
+
if (!isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !contribution || contribution.provider !== event.provider) {
|
|
1511
1790
|
throw { code: "CHANNEL_NOT_DECLARED", message: "channel contribution/provider is not declared" };
|
|
1512
1791
|
}
|
|
1513
1792
|
const key = JSON.stringify([event.channelId, event.connectionId]);
|
|
@@ -1553,16 +1832,29 @@ class ExtensionHostRuntime {
|
|
|
1553
1832
|
return null;
|
|
1554
1833
|
}
|
|
1555
1834
|
async formatChannelReply(input) {
|
|
1556
|
-
const
|
|
1557
|
-
const current = this.channelStates.get(key);
|
|
1558
|
-
const epoch = this.channelEpoch;
|
|
1835
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1559
1836
|
const formatter = this.channelFormatters.get(input.channelId);
|
|
1560
|
-
const isCurrent = () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1561
1837
|
if (!isCurrent() || !formatter) throw { code: "CHANNEL_FORMAT_UNAVAILABLE", message: "channel formatter is not active" };
|
|
1562
1838
|
const result = await formatter(Object.freeze({ ...input }));
|
|
1563
1839
|
if (!isCurrent()) throw { code: "CHANNEL_FORMAT_WITHDRAWN", message: "channel formatter stopped" };
|
|
1840
|
+
if (input.apiVersion === 2 && input.mode === "rendered") return parseChannelRenderedReplyResult(result);
|
|
1564
1841
|
return parseChannelReplyFormatResult(result, input);
|
|
1565
1842
|
}
|
|
1843
|
+
async normalizeChannelMessages(input) {
|
|
1844
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1845
|
+
const normalizer = this.channelNormalizers.get(input.channelId);
|
|
1846
|
+
if (!isCurrent() || !normalizer) throw { code: "CHANNEL_NORMALIZE_UNAVAILABLE", message: "channel normalizer is not active" };
|
|
1847
|
+
const result = await normalizer(Object.freeze({ ...input }));
|
|
1848
|
+
if (!isCurrent()) throw { code: "CHANNEL_NORMALIZE_WITHDRAWN", message: "channel normalizer stopped" };
|
|
1849
|
+
return { items: parseChannelNormalizeResult(input, result) };
|
|
1850
|
+
}
|
|
1851
|
+
/** A formatter or normalizer answers only for the started connection generation it was asked about. */
|
|
1852
|
+
channelCallIsCurrent(input) {
|
|
1853
|
+
const key = JSON.stringify([input.channelId, input.connectionId]);
|
|
1854
|
+
const current = this.channelStates.get(key);
|
|
1855
|
+
const epoch = this.channelEpoch;
|
|
1856
|
+
return () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1857
|
+
}
|
|
1566
1858
|
async withTimeout(operation) {
|
|
1567
1859
|
let timer;
|
|
1568
1860
|
const timeout = new Promise((resolve2) => {
|
|
@@ -1596,6 +1888,7 @@ class ExtensionHostRuntime {
|
|
|
1596
1888
|
const registered = /* @__PURE__ */ new Map();
|
|
1597
1889
|
const registeredChannels = /* @__PURE__ */ new Map();
|
|
1598
1890
|
const registeredFormatters = /* @__PURE__ */ new Map();
|
|
1891
|
+
const registeredNormalizers = /* @__PURE__ */ new Map();
|
|
1599
1892
|
const registeredIntake = /* @__PURE__ */ new Map();
|
|
1600
1893
|
await loaded.activate({
|
|
1601
1894
|
extensionId: this.options.manifest.id,
|
|
@@ -1605,19 +1898,26 @@ class ExtensionHostRuntime {
|
|
|
1605
1898
|
} },
|
|
1606
1899
|
channels: {
|
|
1607
1900
|
register: (channelId, handler) => {
|
|
1608
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1901
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
|
|
1609
1902
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "channel must be declared and registered once" };
|
|
1610
1903
|
}
|
|
1611
1904
|
registeredChannels.set(channelId, handler);
|
|
1612
1905
|
},
|
|
1613
1906
|
registerFormatter: (channelId, formatter) => {
|
|
1614
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1907
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
|
|
1615
1908
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
|
|
1616
1909
|
}
|
|
1617
1910
|
registeredFormatters.set(channelId, formatter);
|
|
1618
1911
|
},
|
|
1912
|
+
registerNormalizer: (channelId, normalizer) => {
|
|
1913
|
+
const manifest = this.options.manifest;
|
|
1914
|
+
if (!this.activating || (manifest.channelApiVersion ?? 0) < 2 || !manifest.permissions.includes("channels.normalize") || !manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof normalizer !== "function" || registeredNormalizers.has(channelId)) {
|
|
1915
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "normalizer needs channel API 2, channels.normalize and one registration" };
|
|
1916
|
+
}
|
|
1917
|
+
registeredNormalizers.set(channelId, normalizer);
|
|
1918
|
+
},
|
|
1619
1919
|
onIntake: (channelId, handler) => {
|
|
1620
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1920
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
|
|
1621
1921
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "intake handler must be declared and registered once" };
|
|
1622
1922
|
}
|
|
1623
1923
|
registeredIntake.set(channelId, handler);
|
|
@@ -1639,7 +1939,9 @@ class ExtensionHostRuntime {
|
|
|
1639
1939
|
for (const [id2, render] of registeredWidgets) this.widgets.set(id2, render);
|
|
1640
1940
|
this.channelHandlers.clear();
|
|
1641
1941
|
this.channelFormatters.clear();
|
|
1942
|
+
this.channelNormalizers.clear();
|
|
1642
1943
|
for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
|
|
1944
|
+
for (const [id2, normalizer] of registeredNormalizers) this.channelNormalizers.set(id2, normalizer);
|
|
1643
1945
|
this.channelIntakeHandlers.clear();
|
|
1644
1946
|
for (const [id2, handler] of registeredIntake) this.channelIntakeHandlers.set(id2, handler);
|
|
1645
1947
|
for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
|