@helpai/elements 0.5.0 → 0.6.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.
- package/elements-web-component.esm.js +17 -17
- package/elements-web-component.esm.js.map +3 -3
- package/elements.cjs.js +24 -24
- package/elements.cjs.js.map +3 -3
- package/elements.esm.js +24 -24
- package/elements.esm.js.map +3 -3
- package/elements.js +18 -18
- package/elements.js.map +3 -3
- package/index.d.ts +1 -1
- package/index.mjs +72 -19
- package/package.json +1 -1
- package/web-component.mjs +72 -19
package/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ interface ClientStorage {
|
|
|
40
40
|
* populated {@link Strings} map. UI components then read `strings.send` as a
|
|
41
41
|
* normal property access — no per-render lookup, no per-render allocation.
|
|
42
42
|
*/
|
|
43
|
-
type StringKey = "launcherOpen" | "panelTitle" | "composerPlaceholder" | "send" | "stop" | "attach" | "micStart" | "micStop" | "micUnsupported" | "expand" | "collapse" | "fullscreen" | "exitFullscreen" | "resizeHandle" | "popOut" | "close" | "moreActions" | "soundOn" | "soundOff" | "language" | "theme" | "themeAuto" | "themeLight" | "themeDark" | "history" | "historyTitle" | "historyEmpty" | "historyLoading" | "historyNewChat" | "historyBack" | "messagesLoading" | "chatClosed" | "startNewConversation" | "dateToday" | "dateYesterday" | "dateLastWeek" | "dateOlder" | "newConversation" | "dropZone" | "attachmentTooLarge" | "attachmentTooMany" | "attachmentMimeRejected" | "errorRetry" | "errorGeneric" | "loading" | "thinking" | "thoughts" | "usedTool" | "collapseSidebar" | "expandSidebar" | "tabHome" | "tabMessages" | "tabHelp" | "tabNews" | "modulesEmpty" | "moduleBack" | "contentLoading" | "homeGreeting" | "homeGreetingNamed" | "homeGreetingLead" | "homeSearchPlaceholder" | "homeContentTitle" | "helpTitle" | "helpSearchPlaceholder" | "helpEmpty" | "helpLoading" | "helpSearchEmpty" | "newsTitle" | "newsEmpty" | "newsLoading" | "newsBack" | "newsPublishedAt";
|
|
43
|
+
type StringKey = "launcherOpen" | "panelTitle" | "composerPlaceholder" | "send" | "stop" | "attach" | "micStart" | "micStop" | "micUnsupported" | "expand" | "collapse" | "fullscreen" | "exitFullscreen" | "resizeHandle" | "popOut" | "close" | "moreActions" | "soundOn" | "soundOff" | "language" | "theme" | "themeAuto" | "themeLight" | "themeDark" | "history" | "historyTitle" | "historyEmpty" | "historyLoading" | "historyNewChat" | "historyBack" | "messagesLoading" | "chatClosed" | "startNewConversation" | "dateToday" | "dateYesterday" | "dateLastWeek" | "dateOlder" | "newConversation" | "dropZone" | "attachmentTooLarge" | "attachmentTooMany" | "attachmentMimeRejected" | "errorRetry" | "errorGeneric" | "errorRateLimited" | "loading" | "thinking" | "thoughts" | "usedTool" | "collapseSidebar" | "expandSidebar" | "tabHome" | "tabMessages" | "tabHelp" | "tabNews" | "modulesEmpty" | "moduleBack" | "contentLoading" | "homeGreeting" | "homeGreetingNamed" | "homeGreetingLead" | "homeSearchPlaceholder" | "homeContentTitle" | "helpTitle" | "helpSearchPlaceholder" | "helpEmpty" | "helpLoading" | "helpSearchEmpty" | "newsTitle" | "newsEmpty" | "newsLoading" | "newsBack" | "newsPublishedAt";
|
|
44
44
|
/** A partial map for one locale — what overrides look like on the wire. */
|
|
45
45
|
type LocaleStrings = Partial<Record<StringKey, string>>;
|
|
46
46
|
/**
|
package/index.mjs
CHANGED
|
@@ -46,6 +46,7 @@ var STRINGS_EN = {
|
|
|
46
46
|
dateYesterday: "Yesterday",
|
|
47
47
|
dropZone: "Drop files to attach",
|
|
48
48
|
errorGeneric: "Something went wrong",
|
|
49
|
+
errorRateLimited: "Too many requests \u2014 please try again in a moment.",
|
|
49
50
|
errorRetry: "Retry",
|
|
50
51
|
expand: "Expand",
|
|
51
52
|
expandSidebar: "Expand sidebar",
|
|
@@ -936,9 +937,10 @@ function fillRandom(view) {
|
|
|
936
937
|
|
|
937
938
|
// src/stream/types.ts
|
|
938
939
|
var StreamError = class extends Error {
|
|
939
|
-
constructor(message, code, cause) {
|
|
940
|
+
constructor(message, code, status, cause) {
|
|
940
941
|
super(message);
|
|
941
942
|
__publicField(this, "code", code);
|
|
943
|
+
__publicField(this, "status", status);
|
|
942
944
|
__publicField(this, "cause", cause);
|
|
943
945
|
this.name = "StreamError";
|
|
944
946
|
}
|
|
@@ -949,7 +951,7 @@ var log3 = logger.scope("parser");
|
|
|
949
951
|
async function* parseChatStream(response, signal6) {
|
|
950
952
|
if (!response.ok) {
|
|
951
953
|
const text = await response.text().catch(() => "");
|
|
952
|
-
throw new StreamError(`server responded ${response.status}: ${text.slice(0, 200)}`, "server");
|
|
954
|
+
throw new StreamError(`server responded ${response.status}: ${text.slice(0, 200)}`, "server", response.status);
|
|
953
955
|
}
|
|
954
956
|
if (!response.body) throw new StreamError("response has no body", "no-body");
|
|
955
957
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
@@ -1113,15 +1115,27 @@ function messageToWireParts(m) {
|
|
|
1113
1115
|
var log4 = logger.scope("transport");
|
|
1114
1116
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
1115
1117
|
var RESUME_BACKOFF_MS = 400;
|
|
1118
|
+
var MAX_REQUEST_RETRIES = 3;
|
|
1119
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
1120
|
+
function backoffMs(attempt) {
|
|
1121
|
+
return RESUME_BACKOFF_MS * 2 ** attempt + Math.floor(Math.random() * 250);
|
|
1122
|
+
}
|
|
1123
|
+
function retryAfterMs(headers) {
|
|
1124
|
+
const raw = headers.get("retry-after");
|
|
1125
|
+
if (!raw) return null;
|
|
1126
|
+
const secs = Number(raw);
|
|
1127
|
+
const ms = Number.isFinite(secs) ? secs * 1e3 : Date.parse(raw) - Date.now();
|
|
1128
|
+
return Number.isFinite(ms) && ms >= 0 ? Math.min(ms, 3e4) : null;
|
|
1129
|
+
}
|
|
1116
1130
|
function sleep(ms, signal6) {
|
|
1117
1131
|
return new Promise((resolve) => {
|
|
1118
|
-
if (signal6
|
|
1132
|
+
if (signal6?.aborted) return resolve();
|
|
1119
1133
|
const id = setTimeout(done, ms);
|
|
1120
1134
|
const onAbort = () => done();
|
|
1121
|
-
signal6
|
|
1135
|
+
signal6?.addEventListener("abort", onAbort, { once: true });
|
|
1122
1136
|
function done() {
|
|
1123
1137
|
clearTimeout(id);
|
|
1124
|
-
signal6
|
|
1138
|
+
signal6?.removeEventListener("abort", onAbort);
|
|
1125
1139
|
resolve();
|
|
1126
1140
|
}
|
|
1127
1141
|
});
|
|
@@ -1375,6 +1389,7 @@ var AgentTransport = class {
|
|
|
1375
1389
|
}
|
|
1376
1390
|
} catch (err) {
|
|
1377
1391
|
if (ctrl.signal.aborted) return true;
|
|
1392
|
+
if (err instanceof StreamError && err.status !== void 0) throw err;
|
|
1378
1393
|
log4.debug("stream segment dropped", { err });
|
|
1379
1394
|
}
|
|
1380
1395
|
return false;
|
|
@@ -1419,13 +1434,23 @@ var AgentTransport = class {
|
|
|
1419
1434
|
url.searchParams.set("resumeAt", String(resumeAt));
|
|
1420
1435
|
return url.toString();
|
|
1421
1436
|
}
|
|
1437
|
+
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1438
|
+
// update-settings / mark-read / start-session), so they auto-retry transient
|
|
1439
|
+
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1440
|
+
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1422
1441
|
async postJson(path, body, label) {
|
|
1423
|
-
const res = await this.
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1442
|
+
const res = await this.fetchWithRetry(
|
|
1443
|
+
() => [
|
|
1444
|
+
this.url(path),
|
|
1445
|
+
{
|
|
1446
|
+
method: "POST",
|
|
1447
|
+
credentials: "omit",
|
|
1448
|
+
headers: { "content-type": "application/json", ...this.opts.auth.headers() },
|
|
1449
|
+
body: JSON.stringify(this.withEnvelope(body))
|
|
1450
|
+
}
|
|
1451
|
+
],
|
|
1452
|
+
label
|
|
1453
|
+
);
|
|
1429
1454
|
return this.assertOk(res, label);
|
|
1430
1455
|
}
|
|
1431
1456
|
async postForm(path, body, label) {
|
|
@@ -1438,16 +1463,44 @@ var AgentTransport = class {
|
|
|
1438
1463
|
return this.assertOk(res, label);
|
|
1439
1464
|
}
|
|
1440
1465
|
async getJson(urlOrPath, label) {
|
|
1441
|
-
const res = await this.
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1466
|
+
const res = await this.fetchWithRetry(
|
|
1467
|
+
() => [
|
|
1468
|
+
this.withEnvelopeQuery(urlOrPath),
|
|
1469
|
+
{ method: "GET", credentials: "omit", headers: { accept: "application/json", ...this.opts.auth.headers() } }
|
|
1470
|
+
],
|
|
1471
|
+
label
|
|
1472
|
+
);
|
|
1446
1473
|
return this.assertOk(res, label);
|
|
1447
1474
|
}
|
|
1448
|
-
/**
|
|
1475
|
+
/**
|
|
1476
|
+
* Fetch with bounded retries for *transient* failures: retryable statuses
|
|
1477
|
+
* (429 / 5xx / …) and network errors, honoring `Retry-After` (else backoff +
|
|
1478
|
+
* jitter). Terminal statuses (4xx) return immediately for the caller to throw.
|
|
1479
|
+
* Use only for idempotent requests.
|
|
1480
|
+
*/
|
|
1481
|
+
async fetchWithRetry(build, label) {
|
|
1482
|
+
for (let attempt = 0; ; attempt++) {
|
|
1483
|
+
const [reqUrl, init2] = build();
|
|
1484
|
+
let res;
|
|
1485
|
+
try {
|
|
1486
|
+
res = await this.fetchImpl(reqUrl, init2);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
if (attempt >= MAX_REQUEST_RETRIES) {
|
|
1489
|
+
throw new StreamError(`${label} failed: network error`, "network", void 0, err);
|
|
1490
|
+
}
|
|
1491
|
+
log4.debug("request network error \u2014 retrying", { label, attempt });
|
|
1492
|
+
await sleep(backoffMs(attempt));
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
|
|
1496
|
+
const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
|
|
1497
|
+
log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
|
|
1498
|
+
await sleep(wait);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/** Throw on non-2xx (with the status attached), otherwise parse the JSON body. */
|
|
1449
1502
|
async assertOk(res, label) {
|
|
1450
|
-
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server");
|
|
1503
|
+
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
|
|
1451
1504
|
return await res.json();
|
|
1452
1505
|
}
|
|
1453
1506
|
url(pathOrUrl) {
|
|
@@ -5453,7 +5506,7 @@ function App({ options, hostElement, bus }) {
|
|
|
5453
5506
|
emitMessage(bus, options, "assistant", assistantText(assistantMsg));
|
|
5454
5507
|
} catch (error) {
|
|
5455
5508
|
assistantMsg.status = "error";
|
|
5456
|
-
assistantMsg.errorText = error instanceof
|
|
5509
|
+
assistantMsg.errorText = error instanceof StreamError && error.status === 429 ? options.strings.errorRateLimited : options.strings.errorGeneric;
|
|
5457
5510
|
feedback.play("error");
|
|
5458
5511
|
bus.emit("error", error);
|
|
5459
5512
|
options.onError?.(error);
|
package/package.json
CHANGED
package/web-component.mjs
CHANGED
|
@@ -46,6 +46,7 @@ var STRINGS_EN = {
|
|
|
46
46
|
dateYesterday: "Yesterday",
|
|
47
47
|
dropZone: "Drop files to attach",
|
|
48
48
|
errorGeneric: "Something went wrong",
|
|
49
|
+
errorRateLimited: "Too many requests \u2014 please try again in a moment.",
|
|
49
50
|
errorRetry: "Retry",
|
|
50
51
|
expand: "Expand",
|
|
51
52
|
expandSidebar: "Expand sidebar",
|
|
@@ -1000,9 +1001,10 @@ function fillRandom(view) {
|
|
|
1000
1001
|
|
|
1001
1002
|
// src/stream/types.ts
|
|
1002
1003
|
var StreamError = class extends Error {
|
|
1003
|
-
constructor(message, code, cause) {
|
|
1004
|
+
constructor(message, code, status, cause) {
|
|
1004
1005
|
super(message);
|
|
1005
1006
|
__publicField(this, "code", code);
|
|
1007
|
+
__publicField(this, "status", status);
|
|
1006
1008
|
__publicField(this, "cause", cause);
|
|
1007
1009
|
this.name = "StreamError";
|
|
1008
1010
|
}
|
|
@@ -1013,7 +1015,7 @@ var log3 = logger.scope("parser");
|
|
|
1013
1015
|
async function* parseChatStream(response, signal6) {
|
|
1014
1016
|
if (!response.ok) {
|
|
1015
1017
|
const text = await response.text().catch(() => "");
|
|
1016
|
-
throw new StreamError(`server responded ${response.status}: ${text.slice(0, 200)}`, "server");
|
|
1018
|
+
throw new StreamError(`server responded ${response.status}: ${text.slice(0, 200)}`, "server", response.status);
|
|
1017
1019
|
}
|
|
1018
1020
|
if (!response.body) throw new StreamError("response has no body", "no-body");
|
|
1019
1021
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
@@ -1177,15 +1179,27 @@ function messageToWireParts(m) {
|
|
|
1177
1179
|
var log4 = logger.scope("transport");
|
|
1178
1180
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
1179
1181
|
var RESUME_BACKOFF_MS = 400;
|
|
1182
|
+
var MAX_REQUEST_RETRIES = 3;
|
|
1183
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
1184
|
+
function backoffMs(attempt) {
|
|
1185
|
+
return RESUME_BACKOFF_MS * 2 ** attempt + Math.floor(Math.random() * 250);
|
|
1186
|
+
}
|
|
1187
|
+
function retryAfterMs(headers) {
|
|
1188
|
+
const raw = headers.get("retry-after");
|
|
1189
|
+
if (!raw) return null;
|
|
1190
|
+
const secs = Number(raw);
|
|
1191
|
+
const ms = Number.isFinite(secs) ? secs * 1e3 : Date.parse(raw) - Date.now();
|
|
1192
|
+
return Number.isFinite(ms) && ms >= 0 ? Math.min(ms, 3e4) : null;
|
|
1193
|
+
}
|
|
1180
1194
|
function sleep(ms, signal6) {
|
|
1181
1195
|
return new Promise((resolve) => {
|
|
1182
|
-
if (signal6
|
|
1196
|
+
if (signal6?.aborted) return resolve();
|
|
1183
1197
|
const id = setTimeout(done, ms);
|
|
1184
1198
|
const onAbort = () => done();
|
|
1185
|
-
signal6
|
|
1199
|
+
signal6?.addEventListener("abort", onAbort, { once: true });
|
|
1186
1200
|
function done() {
|
|
1187
1201
|
clearTimeout(id);
|
|
1188
|
-
signal6
|
|
1202
|
+
signal6?.removeEventListener("abort", onAbort);
|
|
1189
1203
|
resolve();
|
|
1190
1204
|
}
|
|
1191
1205
|
});
|
|
@@ -1439,6 +1453,7 @@ var AgentTransport = class {
|
|
|
1439
1453
|
}
|
|
1440
1454
|
} catch (err) {
|
|
1441
1455
|
if (ctrl.signal.aborted) return true;
|
|
1456
|
+
if (err instanceof StreamError && err.status !== void 0) throw err;
|
|
1442
1457
|
log4.debug("stream segment dropped", { err });
|
|
1443
1458
|
}
|
|
1444
1459
|
return false;
|
|
@@ -1483,13 +1498,23 @@ var AgentTransport = class {
|
|
|
1483
1498
|
url.searchParams.set("resumeAt", String(resumeAt));
|
|
1484
1499
|
return url.toString();
|
|
1485
1500
|
}
|
|
1501
|
+
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1502
|
+
// update-settings / mark-read / start-session), so they auto-retry transient
|
|
1503
|
+
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1504
|
+
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1486
1505
|
async postJson(path, body, label) {
|
|
1487
|
-
const res = await this.
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1506
|
+
const res = await this.fetchWithRetry(
|
|
1507
|
+
() => [
|
|
1508
|
+
this.url(path),
|
|
1509
|
+
{
|
|
1510
|
+
method: "POST",
|
|
1511
|
+
credentials: "omit",
|
|
1512
|
+
headers: { "content-type": "application/json", ...this.opts.auth.headers() },
|
|
1513
|
+
body: JSON.stringify(this.withEnvelope(body))
|
|
1514
|
+
}
|
|
1515
|
+
],
|
|
1516
|
+
label
|
|
1517
|
+
);
|
|
1493
1518
|
return this.assertOk(res, label);
|
|
1494
1519
|
}
|
|
1495
1520
|
async postForm(path, body, label) {
|
|
@@ -1502,16 +1527,44 @@ var AgentTransport = class {
|
|
|
1502
1527
|
return this.assertOk(res, label);
|
|
1503
1528
|
}
|
|
1504
1529
|
async getJson(urlOrPath, label) {
|
|
1505
|
-
const res = await this.
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1530
|
+
const res = await this.fetchWithRetry(
|
|
1531
|
+
() => [
|
|
1532
|
+
this.withEnvelopeQuery(urlOrPath),
|
|
1533
|
+
{ method: "GET", credentials: "omit", headers: { accept: "application/json", ...this.opts.auth.headers() } }
|
|
1534
|
+
],
|
|
1535
|
+
label
|
|
1536
|
+
);
|
|
1510
1537
|
return this.assertOk(res, label);
|
|
1511
1538
|
}
|
|
1512
|
-
/**
|
|
1539
|
+
/**
|
|
1540
|
+
* Fetch with bounded retries for *transient* failures: retryable statuses
|
|
1541
|
+
* (429 / 5xx / …) and network errors, honoring `Retry-After` (else backoff +
|
|
1542
|
+
* jitter). Terminal statuses (4xx) return immediately for the caller to throw.
|
|
1543
|
+
* Use only for idempotent requests.
|
|
1544
|
+
*/
|
|
1545
|
+
async fetchWithRetry(build, label) {
|
|
1546
|
+
for (let attempt = 0; ; attempt++) {
|
|
1547
|
+
const [reqUrl, init] = build();
|
|
1548
|
+
let res;
|
|
1549
|
+
try {
|
|
1550
|
+
res = await this.fetchImpl(reqUrl, init);
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
if (attempt >= MAX_REQUEST_RETRIES) {
|
|
1553
|
+
throw new StreamError(`${label} failed: network error`, "network", void 0, err);
|
|
1554
|
+
}
|
|
1555
|
+
log4.debug("request network error \u2014 retrying", { label, attempt });
|
|
1556
|
+
await sleep(backoffMs(attempt));
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
|
|
1560
|
+
const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
|
|
1561
|
+
log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
|
|
1562
|
+
await sleep(wait);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/** Throw on non-2xx (with the status attached), otherwise parse the JSON body. */
|
|
1513
1566
|
async assertOk(res, label) {
|
|
1514
|
-
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server");
|
|
1567
|
+
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
|
|
1515
1568
|
return await res.json();
|
|
1516
1569
|
}
|
|
1517
1570
|
url(pathOrUrl) {
|
|
@@ -5517,7 +5570,7 @@ function App({ options, hostElement, bus }) {
|
|
|
5517
5570
|
emitMessage(bus, options, "assistant", assistantText(assistantMsg));
|
|
5518
5571
|
} catch (error) {
|
|
5519
5572
|
assistantMsg.status = "error";
|
|
5520
|
-
assistantMsg.errorText = error instanceof
|
|
5573
|
+
assistantMsg.errorText = error instanceof StreamError && error.status === 429 ? options.strings.errorRateLimited : options.strings.errorGeneric;
|
|
5521
5574
|
feedback.play("error");
|
|
5522
5575
|
bus.emit("error", error);
|
|
5523
5576
|
options.onError?.(error);
|