tandem-editor 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -8
- package/README.md +43 -41
- package/dist/channel/index.js +197 -105
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +206 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/{CoworkSettings-C9Dd4D9z.js → CoworkSettings-BlGNryD3.js} +1 -1
- package/dist/client/assets/index-Bnc4LNBi.css +1 -0
- package/dist/client/assets/{index-CN_6DqdC.js → index-D0gSEOxm.js} +51 -51
- package/dist/client/index.html +2 -2
- package/dist/monitor/index.js +3 -3
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +386 -127
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
- package/sample/table-test.md +8 -0
- package/skills/tandem/SKILL.md +7 -7
- package/dist/client/assets/index-Bz3WFCWw.css +0 -1
package/dist/channel/index.js
CHANGED
|
@@ -17914,6 +17914,14 @@ var IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
|
17914
17914
|
var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
17915
17915
|
var CHANNEL_MAX_RETRIES = 5;
|
|
17916
17916
|
var CHANNEL_RETRY_DELAY_MS = 2e3;
|
|
17917
|
+
var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
|
|
17918
|
+
var CHANNEL_SSE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
17919
|
+
var CHANNEL_MODE_FETCH_TIMEOUT_MS = 2e3;
|
|
17920
|
+
var CHANNEL_AWARENESS_FETCH_TIMEOUT_MS = 5e3;
|
|
17921
|
+
var CHANNEL_ERROR_REPORT_TIMEOUT_MS = 3e3;
|
|
17922
|
+
var CHANNEL_REPLY_FETCH_TIMEOUT_MS = 5e3;
|
|
17923
|
+
var CHANNEL_PERMISSION_FETCH_TIMEOUT_MS = 5e3;
|
|
17924
|
+
var CHANNEL_MAX_SSE_BUFFER_BYTES = 1e6;
|
|
17917
17925
|
|
|
17918
17926
|
// src/shared/cli-runtime.ts
|
|
17919
17927
|
function redirectConsoleToStderr() {
|
|
@@ -17945,6 +17953,21 @@ async function authFetch(url, init) {
|
|
|
17945
17953
|
return fetch(url, init);
|
|
17946
17954
|
}
|
|
17947
17955
|
|
|
17956
|
+
// src/shared/fetch-with-timeout.ts
|
|
17957
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
17958
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
17959
|
+
return authFetch(url, { ...init, signal });
|
|
17960
|
+
}
|
|
17961
|
+
function describeFetchError(err, endpoint, timeoutMs) {
|
|
17962
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
17963
|
+
return `${endpoint} timed out after ${timeoutMs}ms`;
|
|
17964
|
+
}
|
|
17965
|
+
return err instanceof Error ? err.message : String(err);
|
|
17966
|
+
}
|
|
17967
|
+
function isAbortOrTimeoutError(err) {
|
|
17968
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
17969
|
+
}
|
|
17970
|
+
|
|
17948
17971
|
// src/shared/events/types.ts
|
|
17949
17972
|
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
17950
17973
|
"annotation:created",
|
|
@@ -17966,9 +17989,9 @@ function formatEventContent(event) {
|
|
|
17966
17989
|
const doc = event.documentId ? ` [doc: ${event.documentId}]` : "";
|
|
17967
17990
|
switch (event.type) {
|
|
17968
17991
|
case "annotation:created": {
|
|
17969
|
-
const { annotationType, content, textSnippet, hasSuggestedText
|
|
17992
|
+
const { annotationType, content, textSnippet, hasSuggestedText } = event.payload;
|
|
17970
17993
|
const snippet = textSnippet ? ` on "${textSnippet}"` : "";
|
|
17971
|
-
const label = hasSuggestedText ? "replacement" :
|
|
17994
|
+
const label = hasSuggestedText ? "replacement" : annotationType;
|
|
17972
17995
|
return `User created ${label}${snippet}: ${content || "(no content)"}${doc}`;
|
|
17973
17996
|
}
|
|
17974
17997
|
case "annotation:accepted": {
|
|
@@ -18063,18 +18086,22 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18063
18086
|
if (retries >= CHANNEL_MAX_RETRIES) {
|
|
18064
18087
|
console.error("[Channel] SSE connection exhausted, reporting error and exiting");
|
|
18065
18088
|
try {
|
|
18066
|
-
await
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18089
|
+
await fetchWithTimeout(
|
|
18090
|
+
`${tandemUrl}/api/channel-error`,
|
|
18091
|
+
{
|
|
18092
|
+
method: "POST",
|
|
18093
|
+
headers: { "Content-Type": "application/json" },
|
|
18094
|
+
body: JSON.stringify({
|
|
18095
|
+
error: "CHANNEL_CONNECT_FAILED",
|
|
18096
|
+
message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
|
|
18097
|
+
})
|
|
18098
|
+
},
|
|
18099
|
+
CHANNEL_ERROR_REPORT_TIMEOUT_MS
|
|
18100
|
+
);
|
|
18074
18101
|
} catch (reportErr) {
|
|
18075
18102
|
console.error(
|
|
18076
18103
|
"[Channel] Could not report failure to server:",
|
|
18077
|
-
reportErr
|
|
18104
|
+
describeFetchError(reportErr, "/api/channel-error", CHANNEL_ERROR_REPORT_TIMEOUT_MS)
|
|
18078
18105
|
);
|
|
18079
18106
|
}
|
|
18080
18107
|
process.exit(1);
|
|
@@ -18086,29 +18113,52 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18086
18113
|
async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
18087
18114
|
const headers = { Accept: "text/event-stream" };
|
|
18088
18115
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
18089
|
-
const
|
|
18116
|
+
const connectCtrl = new AbortController();
|
|
18117
|
+
const connectTimer = setTimeout(
|
|
18118
|
+
() => connectCtrl.abort(new Error("handshake timeout")),
|
|
18119
|
+
CHANNEL_CONNECT_FETCH_TIMEOUT_MS
|
|
18120
|
+
);
|
|
18121
|
+
let res;
|
|
18122
|
+
try {
|
|
18123
|
+
res = await authFetch(`${tandemUrl}/api/events`, { headers, signal: connectCtrl.signal });
|
|
18124
|
+
} finally {
|
|
18125
|
+
clearTimeout(connectTimer);
|
|
18126
|
+
}
|
|
18090
18127
|
if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);
|
|
18091
18128
|
if (!res.body) throw new Error("SSE endpoint returned no body");
|
|
18092
18129
|
const reader = res.body.getReader();
|
|
18093
18130
|
const decoder = new TextDecoder();
|
|
18094
18131
|
let buffer = "";
|
|
18132
|
+
let lastActivityAt = Date.now();
|
|
18133
|
+
let inactivityTimedOut = false;
|
|
18134
|
+
const watchdog = setInterval(() => {
|
|
18135
|
+
if (Date.now() - lastActivityAt > CHANNEL_SSE_INACTIVITY_TIMEOUT_MS) {
|
|
18136
|
+
inactivityTimedOut = true;
|
|
18137
|
+
reader.cancel(new Error("SSE inactivity timeout")).catch(() => {
|
|
18138
|
+
});
|
|
18139
|
+
}
|
|
18140
|
+
}, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
|
|
18095
18141
|
let awarenessTimer = null;
|
|
18096
18142
|
let clearAwarenessTimer = null;
|
|
18097
18143
|
let pendingAwareness = null;
|
|
18098
18144
|
const AWARENESS_CLEAR_MS = 3e3;
|
|
18099
18145
|
function clearAwareness(documentId) {
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18146
|
+
fetchWithTimeout(
|
|
18147
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18148
|
+
{
|
|
18149
|
+
method: "POST",
|
|
18150
|
+
headers: { "Content-Type": "application/json" },
|
|
18151
|
+
body: JSON.stringify({
|
|
18152
|
+
documentId: documentId ?? null,
|
|
18153
|
+
status: "idle",
|
|
18154
|
+
active: false
|
|
18155
|
+
})
|
|
18156
|
+
},
|
|
18157
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18158
|
+
).catch((err) => {
|
|
18109
18159
|
console.error(
|
|
18110
18160
|
"[Channel] clearAwareness failed (non-fatal):",
|
|
18111
|
-
err
|
|
18161
|
+
describeFetchError(err, "/api/channel-awareness clear", CHANNEL_AWARENESS_FETCH_TIMEOUT_MS)
|
|
18112
18162
|
);
|
|
18113
18163
|
});
|
|
18114
18164
|
}
|
|
@@ -18116,16 +18166,27 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18116
18166
|
if (!pendingAwareness) return;
|
|
18117
18167
|
const event = pendingAwareness;
|
|
18118
18168
|
pendingAwareness = null;
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18169
|
+
fetchWithTimeout(
|
|
18170
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18171
|
+
{
|
|
18172
|
+
method: "POST",
|
|
18173
|
+
headers: { "Content-Type": "application/json" },
|
|
18174
|
+
body: JSON.stringify({
|
|
18175
|
+
documentId: event.documentId,
|
|
18176
|
+
status: `processing: ${event.type}`,
|
|
18177
|
+
active: true
|
|
18178
|
+
})
|
|
18179
|
+
},
|
|
18180
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18181
|
+
).catch((err) => {
|
|
18182
|
+
console.error(
|
|
18183
|
+
"[Channel] Awareness update failed:",
|
|
18184
|
+
describeFetchError(
|
|
18185
|
+
err,
|
|
18186
|
+
"/api/channel-awareness update",
|
|
18187
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18188
|
+
)
|
|
18189
|
+
);
|
|
18129
18190
|
});
|
|
18130
18191
|
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18131
18192
|
clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);
|
|
@@ -18135,66 +18196,81 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18135
18196
|
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18136
18197
|
awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
|
|
18137
18198
|
}
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
if (line.startsWith("id: ")) eventId = line.slice(4);
|
|
18151
|
-
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18152
|
-
}
|
|
18153
|
-
if (!data) continue;
|
|
18154
|
-
let event;
|
|
18155
|
-
try {
|
|
18156
|
-
event = parseTandemEvent(JSON.parse(data));
|
|
18157
|
-
} catch {
|
|
18158
|
-
console.error(
|
|
18159
|
-
"[Channel] Malformed SSE event data (skipping), eventId=%s:",
|
|
18160
|
-
eventId,
|
|
18161
|
-
data.slice(0, 200)
|
|
18162
|
-
);
|
|
18163
|
-
if (eventId) onEventId(eventId);
|
|
18164
|
-
continue;
|
|
18165
|
-
}
|
|
18166
|
-
if (!event) {
|
|
18167
|
-
console.error(
|
|
18168
|
-
"[Channel] Invalid SSE event structure (skipping), eventId=%s:",
|
|
18169
|
-
eventId,
|
|
18170
|
-
data.slice(0, 200)
|
|
18199
|
+
try {
|
|
18200
|
+
while (true) {
|
|
18201
|
+
const { done, value } = await reader.read();
|
|
18202
|
+
if (done) {
|
|
18203
|
+
if (inactivityTimedOut) throw new Error("SSE inactivity timeout");
|
|
18204
|
+
throw new Error("SSE stream ended");
|
|
18205
|
+
}
|
|
18206
|
+
lastActivityAt = Date.now();
|
|
18207
|
+
buffer += decoder.decode(value, { stream: true });
|
|
18208
|
+
if (buffer.length > CHANNEL_MAX_SSE_BUFFER_BYTES) {
|
|
18209
|
+
throw new Error(
|
|
18210
|
+
`SSE buffer exceeded ${CHANNEL_MAX_SSE_BUFFER_BYTES} bytes without a frame boundary`
|
|
18171
18211
|
);
|
|
18172
|
-
if (eventId) onEventId(eventId);
|
|
18173
|
-
continue;
|
|
18174
18212
|
}
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18213
|
+
let boundary;
|
|
18214
|
+
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
|
|
18215
|
+
const frame = buffer.slice(0, boundary);
|
|
18216
|
+
buffer = buffer.slice(boundary + 2);
|
|
18217
|
+
if (frame.startsWith(":")) continue;
|
|
18218
|
+
let eventId;
|
|
18219
|
+
let data;
|
|
18220
|
+
for (const line of frame.split("\n")) {
|
|
18221
|
+
if (line.startsWith("id: ")) eventId = line.slice(4);
|
|
18222
|
+
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18223
|
+
}
|
|
18224
|
+
if (!data) continue;
|
|
18225
|
+
let event;
|
|
18226
|
+
try {
|
|
18227
|
+
event = parseTandemEvent(JSON.parse(data));
|
|
18228
|
+
} catch {
|
|
18229
|
+
console.error(
|
|
18230
|
+
"[Channel] Malformed SSE event data (skipping), eventId=%s:",
|
|
18231
|
+
eventId,
|
|
18232
|
+
data.slice(0, 200)
|
|
18233
|
+
);
|
|
18179
18234
|
if (eventId) onEventId(eventId);
|
|
18180
18235
|
continue;
|
|
18181
18236
|
}
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18237
|
+
if (!event) {
|
|
18238
|
+
console.error(
|
|
18239
|
+
"[Channel] Invalid SSE event structure (skipping), eventId=%s:",
|
|
18240
|
+
eventId,
|
|
18241
|
+
data.slice(0, 200)
|
|
18242
|
+
);
|
|
18243
|
+
if (eventId) onEventId(eventId);
|
|
18244
|
+
continue;
|
|
18245
|
+
}
|
|
18246
|
+
if (event.type !== "chat:message") {
|
|
18247
|
+
const mode = await getCachedMode(tandemUrl);
|
|
18248
|
+
if (mode === "solo") {
|
|
18249
|
+
console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
|
|
18250
|
+
if (eventId) onEventId(eventId);
|
|
18251
|
+
continue;
|
|
18189
18252
|
}
|
|
18190
|
-
}
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18253
|
+
}
|
|
18254
|
+
try {
|
|
18255
|
+
await mcp.notification({
|
|
18256
|
+
method: "notifications/claude/channel",
|
|
18257
|
+
params: {
|
|
18258
|
+
content: formatEventContent(event),
|
|
18259
|
+
meta: formatEventMeta(event)
|
|
18260
|
+
}
|
|
18261
|
+
});
|
|
18262
|
+
} catch (err) {
|
|
18263
|
+
console.error("[Channel] MCP notification failed (transport broken?):", err);
|
|
18264
|
+
throw err;
|
|
18265
|
+
}
|
|
18266
|
+
if (eventId) onEventId(eventId);
|
|
18267
|
+
scheduleAwareness(event);
|
|
18194
18268
|
}
|
|
18195
|
-
if (eventId) onEventId(eventId);
|
|
18196
|
-
scheduleAwareness(event);
|
|
18197
18269
|
}
|
|
18270
|
+
} finally {
|
|
18271
|
+
clearInterval(watchdog);
|
|
18272
|
+
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18273
|
+
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18198
18274
|
}
|
|
18199
18275
|
}
|
|
18200
18276
|
var cachedMode = "tandem";
|
|
@@ -18203,7 +18279,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18203
18279
|
const now = Date.now();
|
|
18204
18280
|
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
|
|
18205
18281
|
try {
|
|
18206
|
-
const res = await
|
|
18282
|
+
const res = await fetchWithTimeout(`${tandemUrl}/api/mode`, {}, CHANNEL_MODE_FETCH_TIMEOUT_MS);
|
|
18207
18283
|
if (res.ok) {
|
|
18208
18284
|
const { mode } = await res.json();
|
|
18209
18285
|
cachedMode = mode;
|
|
@@ -18214,7 +18290,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18214
18290
|
} catch (err) {
|
|
18215
18291
|
console.error(
|
|
18216
18292
|
"[Channel] Mode check failed, delivering event (fail-open):",
|
|
18217
|
-
err
|
|
18293
|
+
describeFetchError(err, "/api/mode", CHANNEL_MODE_FETCH_TIMEOUT_MS)
|
|
18218
18294
|
);
|
|
18219
18295
|
cachedModeAt = now;
|
|
18220
18296
|
}
|
|
@@ -18241,7 +18317,7 @@ async function runChannel(opts = {}) {
|
|
|
18241
18317
|
"Event types: annotation:created, annotation:accepted, annotation:dismissed, annotation:reply,",
|
|
18242
18318
|
"chat:message, document:opened, document:closed, document:switched.",
|
|
18243
18319
|
"Chat messages may include a 'selection' field with buffered selection context.",
|
|
18244
|
-
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment,
|
|
18320
|
+
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_edit, etc.) to act on them.",
|
|
18245
18321
|
"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.",
|
|
18246
18322
|
"Do not reply to non-chat events \u2014 just act on them using tools.",
|
|
18247
18323
|
"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback."
|
|
@@ -18275,15 +18351,20 @@ async function runChannel(opts = {}) {
|
|
|
18275
18351
|
if (req.params.name === "tandem_reply") {
|
|
18276
18352
|
const args = req.params.arguments;
|
|
18277
18353
|
try {
|
|
18278
|
-
const res = await
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18354
|
+
const res = await fetchWithTimeout(
|
|
18355
|
+
`${tandemUrl}/api/channel-reply`,
|
|
18356
|
+
{
|
|
18357
|
+
method: "POST",
|
|
18358
|
+
headers: { "Content-Type": "application/json" },
|
|
18359
|
+
body: JSON.stringify(args)
|
|
18360
|
+
},
|
|
18361
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18362
|
+
);
|
|
18283
18363
|
let data;
|
|
18284
18364
|
try {
|
|
18285
18365
|
data = await res.json();
|
|
18286
|
-
} catch {
|
|
18366
|
+
} catch (parseErr) {
|
|
18367
|
+
if (isAbortOrTimeoutError(parseErr)) throw parseErr;
|
|
18287
18368
|
data = { message: "Non-JSON response" };
|
|
18288
18369
|
}
|
|
18289
18370
|
if (!res.ok) {
|
|
@@ -18303,7 +18384,11 @@ async function runChannel(opts = {}) {
|
|
|
18303
18384
|
content: [
|
|
18304
18385
|
{
|
|
18305
18386
|
type: "text",
|
|
18306
|
-
text: `Failed to send reply: ${
|
|
18387
|
+
text: `Failed to send reply: ${describeFetchError(
|
|
18388
|
+
err,
|
|
18389
|
+
"/api/channel-reply",
|
|
18390
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18391
|
+
)}`
|
|
18307
18392
|
}
|
|
18308
18393
|
],
|
|
18309
18394
|
isError: true
|
|
@@ -18323,23 +18408,30 @@ async function runChannel(opts = {}) {
|
|
|
18323
18408
|
});
|
|
18324
18409
|
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
|
|
18325
18410
|
try {
|
|
18326
|
-
const res = await
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18411
|
+
const res = await fetchWithTimeout(
|
|
18412
|
+
`${tandemUrl}/api/channel-permission`,
|
|
18413
|
+
{
|
|
18414
|
+
method: "POST",
|
|
18415
|
+
headers: { "Content-Type": "application/json" },
|
|
18416
|
+
body: JSON.stringify({
|
|
18417
|
+
requestId: params.request_id,
|
|
18418
|
+
toolName: params.tool_name,
|
|
18419
|
+
description: params.description,
|
|
18420
|
+
inputPreview: params.input_preview
|
|
18421
|
+
})
|
|
18422
|
+
},
|
|
18423
|
+
CHANNEL_PERMISSION_FETCH_TIMEOUT_MS
|
|
18424
|
+
);
|
|
18336
18425
|
if (!res.ok) {
|
|
18337
18426
|
console.error(
|
|
18338
18427
|
`[Channel] Permission relay got HTTP ${res.status} \u2014 browser may not see prompt`
|
|
18339
18428
|
);
|
|
18340
18429
|
}
|
|
18341
18430
|
} catch (err) {
|
|
18342
|
-
console.error(
|
|
18431
|
+
console.error(
|
|
18432
|
+
"[Channel] Failed to forward permission request:",
|
|
18433
|
+
describeFetchError(err, "/api/channel-permission", CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
|
|
18434
|
+
);
|
|
18343
18435
|
}
|
|
18344
18436
|
});
|
|
18345
18437
|
console.error(`[Channel] Tandem channel shim starting (server: ${tandemUrl})`);
|