@songsid/agend 2.3.0-beta.21 → 2.3.0-beta.22
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/dist/agent-endpoint.d.ts +8 -0
- package/dist/agent-endpoint.js +11 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +2 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/grok.js +56 -3
- package/dist/backend/grok.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/config-validator.js +8 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +14 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +14 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +28 -2
- package/dist/fleet-manager.js +102 -15
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +2 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +19 -6
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +25 -10
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/provider-secret-registry.d.ts +5 -3
- package/dist/provider-secret-registry.js +8 -2
- package/dist/provider-secret-registry.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +6 -0
- package/dist/public-tunnel-config.js +24 -0
- package/dist/public-tunnel-config.js.map +1 -0
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +3 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +13 -2
- package/dist/public-web-link.js +35 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/secret-store.d.ts +2 -0
- package/dist/secret-store.js +21 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/settings-api.d.ts +1 -0
- package/dist/settings-api.js +13 -9
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +4 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +6 -15
- package/dist/tunnel/cloudflared.js +20 -9
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/ui/app.html +2 -0
- package/dist/ui/chat-render.js +39 -0
- package/dist/ui/chat-store.js +8 -0
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/fleet-canvas.js +14 -6
- package/dist/ui/fleet-route-scene.js +86 -21
- package/dist/ui/fleet-traffic.js +57 -10
- package/dist/ui/icons/icon-192.png +0 -0
- package/dist/ui/icons/icon-512.png +0 -0
- package/dist/ui/panel-settings.js +57 -4
- package/dist/ui/settings-appearance.js +126 -1
- package/dist/ui/settings-learn.js +1 -0
- package/dist/ui/settings-strings.js +78 -0
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +47 -21
- package/dist/ui/signin.html +2 -0
- package/dist/web-api.d.ts +6 -2
- package/dist/web-api.js +114 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon-worker.d.ts +1 -0
- package/dist/web-app-icon-worker.js +15 -0
- package/dist/web-app-icon-worker.js.map +1 -0
- package/dist/web-app-icon.d.ts +107 -0
- package/dist/web-app-icon.js +256 -0
- package/dist/web-app-icon.js.map +1 -0
- package/dist/web-chat-history.d.ts +23 -0
- package/dist/web-chat-history.js +32 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +7 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-icons.d.ts +33 -1
- package/dist/web-icons.js +83 -11
- package/dist/web-icons.js.map +1 -1
- package/dist/web-png.d.ts +29 -0
- package/dist/web-png.js +321 -0
- package/dist/web-png.js.map +1 -0
- package/dist/web-reactions.d.ts +34 -0
- package/dist/web-reactions.js +47 -0
- package/dist/web-reactions.js.map +1 -0
- package/package.json +1 -1
|
@@ -13,6 +13,18 @@ register("settings", {
|
|
|
13
13
|
publicLinkHint: "Explicit admin consent on every request. Cloudflare handles TLS; link users have full web-admin access. HTML previews get a second temporary tunnel unless web.preview_on_public_link is false.",
|
|
14
14
|
publicTtl: "Public link minutes (1–480)",
|
|
15
15
|
publicProtocol: "Tunnel protocol",
|
|
16
|
+
namedHostname: "Fixed public hostname (optional)",
|
|
17
|
+
namedOriginPort: "Named tunnel origin port",
|
|
18
|
+
namedHostnameInvalid: "Enter a DNS hostname or HTTPS root URL.",
|
|
19
|
+
namedPortInvalid: "Use an integer port from 1024 to 65535.",
|
|
20
|
+
namedTunnelHint: "Blank hostname uses a quick tunnel. Named routes point to this loopback port. Opening, expiry and sign-in still apply.",
|
|
21
|
+
namedTunnelSetup: "How to set this up",
|
|
22
|
+
namedToken: "Cloudflare connector token",
|
|
23
|
+
namedTokenHint: "Stored privately, never shown again. Format checked locally; Cloudflare credentials and routing are checked when opening. Saving or clearing closes the current public link.",
|
|
24
|
+
namedTokenSet: "Set token",
|
|
25
|
+
namedTokenClear: "Clear token",
|
|
26
|
+
awaitingConfirmation: "Waiting for confirmation.",
|
|
27
|
+
namedTokenSaved: "Token change saved. Reopen the public link.",
|
|
16
28
|
webEcho: "Web echo",
|
|
17
29
|
webEchoHint: "Copies web messages to the bound Telegram or Discord topic. ClassicBot rooms follow it unless a room sets its own.",
|
|
18
30
|
webUsagePublic: "Usage report over the public link", webUsagePublicHint: "Fleet → Usage (up to 400 days of each agent's tokens, requests, messages and API-equivalent amounts) is offered over the temporary public link too. Off by default; guests never see it. Applies at once.",
|
|
@@ -379,6 +391,33 @@ register("settings", {
|
|
|
379
391
|
modProposedBy: "{0} proposes",
|
|
380
392
|
modReview: "Review",
|
|
381
393
|
modDismiss: "Dismiss",
|
|
394
|
+
appIconTitle: "App icon",
|
|
395
|
+
appIconLead: "Shown on browser tabs and phone home screens. Square, 512\u00a0px or more.",
|
|
396
|
+
appIconCurrent: "The app icon now",
|
|
397
|
+
appIconDefault: "The AgEnD icon (default)",
|
|
398
|
+
appIconCustom: "Your icon",
|
|
399
|
+
appIconPreview: "The new icon",
|
|
400
|
+
appIconChoose: "Choose image…",
|
|
401
|
+
appIconUse: "Use this icon",
|
|
402
|
+
appIconCancel: "Cancel",
|
|
403
|
+
appIconReset: "Restore default",
|
|
404
|
+
appIconSaved: "App icon changed",
|
|
405
|
+
appIconResetDone: "App icon back to the default",
|
|
406
|
+
appIconLow: "Smaller than 512\u00a0px: it is enlarged and may look soft.",
|
|
407
|
+
appIconNotAnImage: "That file could not be read as an image.",
|
|
408
|
+
appIconLoadFailed: "The app icon could not be read.",
|
|
409
|
+
appIconReadOnly: "Only an admin's session can change it.",
|
|
410
|
+
appIconWho: "Anyone who can open this address sees it: no private pictures.",
|
|
411
|
+
appIconIos: "On an iPhone, add the home-screen shortcut again to see a change.",
|
|
412
|
+
appIconError_app_icon_too_large: "The image is too large ({0}).",
|
|
413
|
+
appIconError_app_icon_not_png: "The host takes a PNG ({0}).",
|
|
414
|
+
appIconError_app_icon_unreadable: "The image could not be used: {0}.",
|
|
415
|
+
appIconError_app_icon_admin_only: "Only an admin's session can change the app icon.",
|
|
416
|
+
appIconError_app_icon_needs_a_signed_in_person: "Sign in to change the app icon.",
|
|
417
|
+
appIconError_app_icon_busy: "Another icon is being made; try again in a moment.",
|
|
418
|
+
appIconError_app_icon_superseded: "The icon was reset meanwhile; choose the picture again.",
|
|
419
|
+
appIconError_app_icon_not_reset: "The default could not be restored ({0}).",
|
|
420
|
+
appIconError_other: "The app icon was not changed ({0}).",
|
|
382
421
|
cssTitle: "Custom CSS (yours only)",
|
|
383
422
|
cssLead: "Applies to your screens only.",
|
|
384
423
|
cssLoadFailed: "Couldn't load your custom CSS. Reload the page to try again.",
|
|
@@ -574,6 +613,18 @@ register("settings", {
|
|
|
574
613
|
publicLinkHint: "每次都需管理員明確啟用。Cloudflare 終止 TLS;登入者具有完整網頁管理權限。HTML 預覽會另開一條臨時通道,除非設定 web.preview_on_public_link: false。",
|
|
575
614
|
publicTtl: "公開連結分鐘數(1–480)",
|
|
576
615
|
publicProtocol: "Tunnel 協定",
|
|
616
|
+
namedHostname: "固定公開網域(選用)",
|
|
617
|
+
namedOriginPort: "Named tunnel 本機連接埠",
|
|
618
|
+
namedHostnameInvalid: "請輸入 DNS 網域名稱或 HTTPS 根網址。",
|
|
619
|
+
namedPortInvalid: "請使用 1024 到 65535 的整數連接埠。",
|
|
620
|
+
namedTunnelHint: "網域留空時使用 quick tunnel。Named 路由需指向此本機連接埠;仍須開啟連結,並遵守到期及登入規則。",
|
|
621
|
+
namedTunnelSetup: "設定教學",
|
|
622
|
+
namedToken: "Cloudflare 連接器 token",
|
|
623
|
+
namedTokenHint: "私密儲存,不會讀回。儲存時只檢查格式,開啟時才確認憑證與路由。設定或清除會關閉目前的公開連結。",
|
|
624
|
+
namedTokenSet: "設定 token",
|
|
625
|
+
namedTokenClear: "清除 token",
|
|
626
|
+
awaitingConfirmation: "等待確認中。",
|
|
627
|
+
namedTokenSaved: "Token 變更已儲存,請重新開啟公開連結。",
|
|
577
628
|
webEcho: "Web 同步",
|
|
578
629
|
webEchoHint: "將網頁訊息複製到綁定的 Telegram 或 Discord 主題。ClassicBot 聊天室也照這個設定,除非該聊天室另外設定。",
|
|
579
630
|
webUsagePublic: "透過公開連結提供用量報告", webUsagePublicHint: "Fleet → 用量(最多 400 天、每個 agent 的 token、請求、訊息與依牌價換算的 API 等值)也會透過臨時公開連結提供。預設關閉;訪客一律看不到。變更立即生效。",
|
|
@@ -914,6 +965,33 @@ register("settings", {
|
|
|
914
965
|
modProposedBy: "{0} 提議",
|
|
915
966
|
modReview: "檢視",
|
|
916
967
|
modDismiss: "略過",
|
|
968
|
+
appIconTitle: "App 圖示",
|
|
969
|
+
appIconLead: "顯示在瀏覽器分頁和手機主畫面。正方形,512\u00a0px 以上。",
|
|
970
|
+
appIconCurrent: "目前的 App 圖示",
|
|
971
|
+
appIconDefault: "AgEnD 圖示(預設)",
|
|
972
|
+
appIconCustom: "你的圖示",
|
|
973
|
+
appIconPreview: "新的圖示",
|
|
974
|
+
appIconChoose: "選擇圖片…",
|
|
975
|
+
appIconUse: "使用這個圖示",
|
|
976
|
+
appIconCancel: "取消",
|
|
977
|
+
appIconReset: "恢復預設",
|
|
978
|
+
appIconSaved: "App 圖示已更換",
|
|
979
|
+
appIconResetDone: "App 圖示已恢復預設",
|
|
980
|
+
appIconLow: "小於 512\u00a0px:會被放大,可能不夠清晰。",
|
|
981
|
+
appIconNotAnImage: "無法把這個檔案讀成圖片。",
|
|
982
|
+
appIconLoadFailed: "讀不到 App 圖示的設定。",
|
|
983
|
+
appIconReadOnly: "只有管理者的 session 能更換。",
|
|
984
|
+
appIconWho: "能打開這個網址的人都看得到:請別用私人照片。",
|
|
985
|
+
appIconIos: "iPhone 主畫面要刪掉捷徑重新加入才會換。",
|
|
986
|
+
appIconError_app_icon_too_large: "圖片太大({0})。",
|
|
987
|
+
appIconError_app_icon_not_png: "主機只接受 PNG({0})。",
|
|
988
|
+
appIconError_app_icon_unreadable: "這張圖無法使用:{0}。",
|
|
989
|
+
appIconError_app_icon_admin_only: "只有管理者的 session 能更換 App 圖示。",
|
|
990
|
+
appIconError_app_icon_needs_a_signed_in_person: "請先登入再更換 App 圖示。",
|
|
991
|
+
appIconError_app_icon_busy: "另一張圖示正在處理中,請稍後再試。",
|
|
992
|
+
appIconError_app_icon_superseded: "圖示剛被恢復成預設,請重新選擇圖片。",
|
|
993
|
+
appIconError_app_icon_not_reset: "無法恢復預設({0})。",
|
|
994
|
+
appIconError_other: "App 圖示沒有更換({0})。",
|
|
917
995
|
cssTitle: "自訂 CSS(只套用在你自己)",
|
|
918
996
|
cssLead: "只套用在你的畫面。",
|
|
919
997
|
cssLoadFailed: "無法載入你的自訂 CSS。請重新整理頁面再試一次。",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// org canvas reads /ui/traffic when it has no stream).
|
|
35
35
|
// #2233: backend_login — a dashboard sign-in started, moved or ended (no details: the page reads its own view).
|
|
36
36
|
// tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
|
|
37
|
-
const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks"];
|
|
37
|
+
const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks"];
|
|
38
38
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
39
39
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
40
40
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|
|
@@ -128,6 +128,7 @@ export function createStream(opts) {
|
|
|
128
128
|
if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
|
|
129
129
|
if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
|
|
130
130
|
if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
|
|
131
|
+
if (Array.isArray(d.reactions)) for (const u of d.reactions) emit("reactions", u); // #2305
|
|
131
132
|
if (Array.isArray(d.needs)) emit("needs", { items: d.needs });
|
|
132
133
|
return true;
|
|
133
134
|
} catch { return false; /* the next tick, or the catch-up's retry, tries again */ }
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -124,6 +124,17 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
124
124
|
.idc.id-bar { position: relative; }
|
|
125
125
|
.idc.id-bar::before { content: ""; position: absolute; left: 0; top: var(--s-2); bottom: var(--s-2); width: 3px; background: var(--id); pointer-events: none; }
|
|
126
126
|
.idc.id-name .inst-name { color: var(--id-name); }
|
|
127
|
+
/* #2330: the same straight bar for every other coloured left edge (a 3px border-left curved into a rounded box's
|
|
128
|
+
corners). `.lbar` is the utility; the places below join its selector list instead of carrying a class. A place sets
|
|
129
|
+
--lbar-color (status variants too) and adds the 3px the border took to its own padding-left, so its text stays put.
|
|
130
|
+
A short box that clips to its corners (.msg-quote) moves the bar in (--lbar-left) and shortens it (--lbar-inset) so
|
|
131
|
+
the corner never cuts its ends round.
|
|
132
|
+
tests/web-left-bar-2330.test.ts keeps a coloured border-left off rounded boxes. */
|
|
133
|
+
.lbar, .reply-bar, .msg-quote, .p-log .log-item.k-error, .p-log .log-item.s-failed, .org-node, .us-basis, .us-partial, .cmd-card.bad, .tb-tile { position: relative; }
|
|
134
|
+
.lbar::before, .reply-bar::before, .msg-quote::before, .p-log .log-item.k-error::before, .p-log .log-item.s-failed::before, .org-node::before,
|
|
135
|
+
.us-basis::before, .us-partial::before, .cmd-card.bad::before, .tb-tile::before {
|
|
136
|
+
content: ""; position: absolute; left: var(--lbar-left, 0); top: var(--lbar-inset, var(--s-2)); bottom: var(--lbar-inset, var(--s-2)); width: 3px;
|
|
137
|
+
background: var(--lbar-color, var(--accent)); pointer-events: none; }
|
|
127
138
|
.idc-swatches, .idc-places { border: 0; padding: 0; margin: 0 0 var(--s-3); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
128
139
|
.idc-swatches legend, .idc-places legend { font-weight: var(--fw-semibold); margin-bottom: 6px; width: 100%; }
|
|
129
140
|
.idc-swatch { --sw: var(--sw-dark); width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; background: var(--sw); cursor: pointer; padding: 0; }
|
|
@@ -139,6 +150,14 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
139
150
|
.s-ap-group { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); margin: var(--s-4) 0 var(--s-2); }
|
|
140
151
|
.s-ap-group:first-child { margin-top: 0; }
|
|
141
152
|
.s-ap-title { display: flex; align-items: center; gap: var(--s-2); }
|
|
153
|
+
/* #2308: the app icon card — the icon now (home-screen size, and tab size) beside the one chosen, before it is used. */
|
|
154
|
+
.s-app-icon-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-4); margin: var(--s-2) 0; }
|
|
155
|
+
.s-app-icon-row figure { margin: 0; display: grid; justify-items: center; gap: var(--s-1); }
|
|
156
|
+
.s-app-icon-row figure.s-app-icon-now { grid-template-columns: auto auto; align-items: end; column-gap: var(--s-3); }
|
|
157
|
+
.s-app-icon-row figure.s-app-icon-now figcaption { grid-column: 1 / -1; }
|
|
158
|
+
.s-app-icon-row figcaption { font-size: var(--fs-sm); color: var(--text-2); }
|
|
159
|
+
.s-app-icon-big { width: 96px; height: 96px; border-radius: 22%; border: 1px solid var(--border); background: var(--surface-2); object-fit: cover; }
|
|
160
|
+
.s-app-icon-small { width: 32px; height: 32px; border-radius: 6px; object-fit: cover; }
|
|
142
161
|
/* The fields' placeholders show the defaults: readable, not the browser's faint grey (dark mode). */
|
|
143
162
|
.s-appearance input::placeholder { color: var(--text-3); opacity: 1; }
|
|
144
163
|
/* A card title in Chinese is never upper-cased ("AGENT 的主題"): the small caps are a Latin convention. */
|
|
@@ -284,7 +303,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
284
303
|
.p-log .log-item.turn-start { margin-top: var(--s-4); }
|
|
285
304
|
.p-log .log-item.k-user { background: var(--user-bubble); }
|
|
286
305
|
.p-log .log-item.k-turn_end, .p-log .log-item.k-state, .p-log .log-item.k-event { background: transparent; color: var(--text-3); padding-block: 2px; font-size: var(--fs-sm); }
|
|
287
|
-
.p-log .log-item.k-error, .p-log .log-item.s-failed {
|
|
306
|
+
.p-log .log-item.k-error, .p-log .log-item.s-failed { --lbar-color: var(--danger); padding-left: calc(var(--s-3) + 3px); }
|
|
288
307
|
.p-log .log-head { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
|
|
289
308
|
.p-log .log-icon { flex: none; }
|
|
290
309
|
.p-log .log-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -633,8 +652,8 @@ img.amb-p { object-fit: contain; }
|
|
|
633
652
|
background: var(--surface); box-shadow: var(--shadow-1); opacity: 0; pointer-events: none; transition: opacity var(--t-fast); }
|
|
634
653
|
.msg:hover > .msg-actions, .msg:focus-within > .msg-actions { opacity: 1; pointer-events: auto; }
|
|
635
654
|
@media (hover: none) { .msg-actions { display: none; } }
|
|
636
|
-
.msg-quote { display: flex; align-items: center; gap: 4px; max-width: 100%; margin: 0 0 4px; padding: 2px var(--s-2); border: 0;
|
|
637
|
-
|
|
655
|
+
.msg-quote { display: flex; align-items: center; gap: 4px; max-width: 100%; margin: 0 0 4px; padding: 2px var(--s-2) 2px calc(var(--s-2) + 3px); border: 0;
|
|
656
|
+
--lbar-color: var(--border-strong, var(--border)); --lbar-inset: 4px; --lbar-left: 2px; border-radius: var(--r-sm); background: transparent; color: var(--text-3);
|
|
638
657
|
font: inherit; font-size: var(--fs-sm); text-align: left; cursor: pointer; overflow: hidden; }
|
|
639
658
|
.msg.user .msg-quote { margin-left: auto; }
|
|
640
659
|
.msg-quote:hover, .msg-quote:focus-visible { color: var(--text-2); background: var(--surface-2); outline: none; }
|
|
@@ -760,8 +779,8 @@ img.amb-p { object-fit: contain; }
|
|
|
760
779
|
.guest-note { margin: 2px 0 0; font-size: var(--fs-xs); color: var(--text-3); text-align: center; }
|
|
761
780
|
.pending-files { display: flex; flex-wrap: wrap; gap: var(--s-2); padding: var(--s-2) var(--s-2) 0; }
|
|
762
781
|
/* #2272: replying — the bar above the composer (who and what is answered; ✕ / Esc cancels). */
|
|
763
|
-
.reply-bar { display: flex; align-items: center; gap: var(--s-2); min-width: 0; margin: var(--s-2) var(--s-2) 0; padding: 4px var(--s-2) 4px var(--s-3);
|
|
764
|
-
|
|
782
|
+
.reply-bar { display: flex; align-items: center; gap: var(--s-2); min-width: 0; margin: var(--s-2) var(--s-2) 0; padding: 4px var(--s-2) 4px calc(var(--s-3) + 3px);
|
|
783
|
+
--lbar-color: var(--accent); border-radius: var(--r-sm); background: var(--surface-2); font-size: var(--fs-sm); }
|
|
765
784
|
.reply-bar .reply-mark { color: var(--accent); }
|
|
766
785
|
.reply-bar .reply-label { font-weight: var(--fw-semibold); white-space: nowrap; }
|
|
767
786
|
.reply-bar .reply-text { flex: 1; min-width: 0; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -1281,10 +1300,10 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
|
|
|
1281
1300
|
.org-team-head h3 { display: flex; align-items: center; gap: var(--s-2); font-size: var(--fs-md); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
|
|
1282
1301
|
.org-team-desc { color: var(--text-2); font-size: var(--fs-sm); overflow-wrap: anywhere; }
|
|
1283
1302
|
.org-team-sub { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-3); font-size: var(--fs-xs); }
|
|
1284
|
-
.org-node { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; padding: var(--s-3); border-radius: var(--r-md); background: var(--surface);
|
|
1285
|
-
.org-node.st-needs, .org-node.st-stuck {
|
|
1286
|
-
.org-node.st-crashed {
|
|
1287
|
-
.org-node.st-working {
|
|
1303
|
+
.org-node { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; padding: var(--s-3) var(--s-3) var(--s-3) calc(var(--s-3) + 3px); border-radius: var(--r-md); background: var(--surface); --lbar-color: transparent; }
|
|
1304
|
+
.org-node.st-needs, .org-node.st-stuck { --lbar-color: var(--warn); }
|
|
1305
|
+
.org-node.st-crashed { --lbar-color: var(--danger); }
|
|
1306
|
+
.org-node.st-working { --lbar-color: var(--ok); }
|
|
1288
1307
|
.org-node.st-missing { opacity: 0.7; }
|
|
1289
1308
|
.org-node-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
|
|
1290
1309
|
.org-names { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
@@ -1377,16 +1396,17 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1377
1396
|
.oc { --fx-task: #b794f6; --fx-send: #2dd4bf; --fx-report: #f472b6; --fx-broadcast: #fb923c; --fx-team: #cbd5e1; }
|
|
1378
1397
|
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .oc { --fx-task: #7c3aed; --fx-send: #0f766e; --fx-report: #be185d; --fx-broadcast: #c2410c; --fx-team: #475569; } }
|
|
1379
1398
|
:root[data-theme="light"] .oc { --fx-task: #7c3aed; --fx-send: #0f766e; --fx-report: #be185d; --fx-broadcast: #c2410c; --fx-team: #475569; }
|
|
1380
|
-
|
|
1399
|
+
/* #2329: a line's stroke width is its own attribute (fleet-traffic.js strokeOf): its head and its trimmed ends are sized from it. */
|
|
1400
|
+
.oc-flow { fill: none; stroke-linecap: round; }
|
|
1381
1401
|
.oc-flow-halo { fill: none; stroke: var(--bg); stroke-width: 6; stroke-linecap: round; opacity: 0.85; }
|
|
1382
1402
|
.oc-flow.h-task { stroke: var(--fx-task); } .oc-flow.h-send { stroke: var(--fx-send); } .oc-flow.h-report { stroke: var(--fx-report); }
|
|
1383
|
-
.oc-flow.h-broadcast { stroke: var(--fx-broadcast); } .oc-flow.h-team { stroke: var(--fx-team);
|
|
1403
|
+
.oc-flow.h-broadcast { stroke: var(--fx-broadcast); } .oc-flow.h-team { stroke: var(--fx-team); }
|
|
1384
1404
|
.oc-flow.ph-live { stroke-dasharray: 7 6; animation: oc-dash 0.9s linear infinite; }
|
|
1385
1405
|
.oc-flow.ph-queued { stroke-dasharray: 2 5; opacity: 0.8; }
|
|
1386
1406
|
.oc-flow.ph-fading { stroke: var(--text-3); animation: oc-fade 12s ease-out forwards; }
|
|
1387
1407
|
/* #1918 review: the work — a delegation in progress (a slow dash, kept until its report), its report coming back, none. */
|
|
1388
|
-
.oc-flow.ph-work { stroke-dasharray: 10 7;
|
|
1389
|
-
.oc-flow.ph-return {
|
|
1408
|
+
.oc-flow.ph-work { stroke-dasharray: 10 7; animation: oc-dash 2.4s linear infinite; }
|
|
1409
|
+
.oc-flow.ph-return { animation: oc-fade 2.4s ease-out forwards; }
|
|
1390
1410
|
.oc-flow.ph-stale { stroke: var(--text-3); stroke-dasharray: 3 6; animation: oc-fade 12s ease-out forwards; }
|
|
1391
1411
|
.oc-ah.h-task { fill: var(--fx-task); } .oc-ah.h-send { fill: var(--fx-send); } .oc-ah.h-report { fill: var(--fx-report); }
|
|
1392
1412
|
.oc-ah.h-broadcast { fill: var(--fx-broadcast); } .oc-ah.h-team { fill: var(--fx-team); } .oc-ah.h-fading { fill: var(--text-3); }
|
|
@@ -1483,9 +1503,9 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1483
1503
|
.us-csv { margin-left: auto; }
|
|
1484
1504
|
.us-custom { display: flex; flex-wrap: wrap; align-items: end; gap: var(--s-2) var(--s-3); font-size: var(--fs-sm); }
|
|
1485
1505
|
.us-custom label { display: inline-flex; flex-direction: column; gap: 2px; color: var(--text-2); }
|
|
1486
|
-
.us-basis { font-size: var(--fs-sm); color: var(--text); padding: var(--s-2) var(--s-3)
|
|
1506
|
+
.us-basis { font-size: var(--fs-sm); color: var(--text); padding: var(--s-2) var(--s-3) var(--s-2) calc(var(--s-3) + 3px); --lbar-color: var(--accent); background: var(--surface-2); border-radius: var(--r-sm); }
|
|
1487
1507
|
.us-fresh { font-size: var(--fs-xs); }
|
|
1488
|
-
.us-partial { font-size: var(--fs-sm); padding: var(--s-2) var(--s-3)
|
|
1508
|
+
.us-partial { font-size: var(--fs-sm); padding: var(--s-2) var(--s-3) var(--s-2) calc(var(--s-3) + 3px); --lbar-color: var(--warn); background: var(--warn-bg); border-radius: var(--r-sm); }
|
|
1489
1509
|
.us-unpriced { font-size: var(--fs-xs); }
|
|
1490
1510
|
.us-scan { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
|
|
1491
1511
|
.us-totals { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-3); margin: 0; }
|
|
@@ -1551,7 +1571,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1551
1571
|
.cmd-palette .cmd-arg { color: var(--text-3); }
|
|
1552
1572
|
.cmd-palette .cmd-none { color: var(--text-3); cursor: default; }
|
|
1553
1573
|
.cmd-card { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-2); padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface); font-size: var(--fs-sm); }
|
|
1554
|
-
.cmd-card.bad {
|
|
1574
|
+
.cmd-card.bad { --lbar-color: var(--danger); padding-left: calc(var(--s-3) + 3px); }
|
|
1555
1575
|
.cmd-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
1556
1576
|
.cmd-name { font-weight: var(--fw-semibold); }
|
|
1557
1577
|
.cmd-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-ui); color: var(--text-2); }
|
|
@@ -1900,12 +1920,12 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1900
1920
|
.tb-count { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text-3); }
|
|
1901
1921
|
.tb-drop-line { height: 3px; border-radius: 2px; background: var(--accent); }
|
|
1902
1922
|
|
|
1903
|
-
.tb-tile { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-3); border-radius: var(--r-md); background: var(--bg); border: 1px solid var(--border);
|
|
1923
|
+
.tb-tile { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-3) var(--s-3) var(--s-3) calc(var(--s-3) + 2px); border-radius: var(--r-md); background: var(--bg); border: 1px solid var(--border); --lbar-color: var(--text-3); cursor: pointer; font-size: var(--fs-sm); min-width: 0; }
|
|
1904
1924
|
.tb-tile:hover { border-color: var(--border-strong); }
|
|
1905
|
-
.tb-tile.st-claimed {
|
|
1906
|
-
.tb-tile.st-blocked {
|
|
1907
|
-
.tb-tile.st-review {
|
|
1908
|
-
.tb-tile.st-done {
|
|
1925
|
+
.tb-tile.st-claimed { --lbar-color: var(--accent); }
|
|
1926
|
+
.tb-tile.st-blocked { --lbar-color: var(--danger); }
|
|
1927
|
+
.tb-tile.st-review { --lbar-color: var(--warn); }
|
|
1928
|
+
.tb-tile.st-done { --lbar-color: var(--ok); }
|
|
1909
1929
|
.tb-tile.dragging { opacity: 0.4; }
|
|
1910
1930
|
.tb-tile.flash { animation: tb-flash 1.6s var(--ease); }
|
|
1911
1931
|
@keyframes tb-flash { from { box-shadow: 0 0 0 3px var(--accent-dim); background: var(--accent-dim); } to { box-shadow: none; } }
|
|
@@ -2006,3 +2026,9 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
2006
2026
|
.tb-tile.flash { animation: none; }
|
|
2007
2027
|
.tb-chev { transition: none; }
|
|
2008
2028
|
}
|
|
2029
|
+
|
|
2030
|
+
/* #2305: the agent's reactions to a message (its minimal reply), under the bubble — on the person's side for theirs. */
|
|
2031
|
+
.msg-reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
2032
|
+
.msg.user .msg-reactions { justify-content: flex-end; }
|
|
2033
|
+
.msg-reactions .reaction { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 24px; padding: 0 6px;
|
|
2034
|
+
border: 1px solid var(--border); border-radius: var(--r-full); background: var(--surface-2); font-family: var(--font-emoji); font-size: 15px; line-height: 1; }
|
package/dist/ui/signin.html
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
<meta name="referrer" content="no-referrer">
|
|
7
7
|
<meta name="robots" content="noindex">
|
|
8
8
|
<title>AgEnD — Sign in</title>
|
|
9
|
+
<!--app-icon-->
|
|
9
10
|
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 64x64">
|
|
10
11
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any">
|
|
11
12
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
|
13
|
+
<!--/app-icon-->
|
|
12
14
|
<script src="/assets/theme.js"></script>
|
|
13
15
|
<link rel="stylesheet" href="/assets/tokens.css">
|
|
14
16
|
<style>
|
package/dist/web-api.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { CachedImage } from "./web-images.js";
|
|
|
4
4
|
import type { SettingsExecution } from "./settings-transaction.js";
|
|
5
5
|
import type { PaneContextSource } from "./pane-context-cache.js";
|
|
6
6
|
import type { OwnerCssStore } from "./web-owner-css-store.js";
|
|
7
|
+
import { type AppIconStore } from "./web-app-icon.js";
|
|
7
8
|
import type { ModStore } from "./web-mod-store.js";
|
|
8
9
|
import type { WebEffectService } from "./web-effect-service.js";
|
|
9
10
|
import type { ThemeStore } from "./web-theme-store.js";
|
|
@@ -130,9 +131,10 @@ export interface WebApiContext {
|
|
|
130
131
|
* (#1320 part B). Resolves the entries and adapters itself; returns how
|
|
131
132
|
* many channels were posted to. Never rejects the web send on failure.
|
|
132
133
|
*/
|
|
133
|
-
sendClassicWebEcho?(instance: string, text: string, sender?: string): Promise<number>;
|
|
134
|
+
sendClassicWebEcho?(instance: string, text: string, sender?: string, copies?: import("./web-reactions.js").WebEchoCopy[]): Promise<number>;
|
|
134
135
|
/** #1920 §5.2: a web message's channel copy was sent — guests of that channel may see it now. */
|
|
135
|
-
|
|
136
|
+
/** #2305: `copies`, the copies' own message ids, so a reaction to the web message is put on them too. */
|
|
137
|
+
webMessageInChannel?(instance: string, messageId: string, copies?: readonly import("./web-reactions.js").WebEchoCopy[]): void;
|
|
136
138
|
/** false: definitely not delivered (the instance's IPC is gone, or it was restarted meanwhile). */
|
|
137
139
|
deliverToInstance(instanceName: string, payload: Record<string, unknown>, options?: {
|
|
138
140
|
stillCurrent?: () => boolean;
|
|
@@ -177,6 +179,8 @@ export interface WebApiContext {
|
|
|
177
179
|
readonly webPrefs?: WebPrefsStore;
|
|
178
180
|
/** #1629 r4 §3: each person's own CSS (human sessions only; never the public link, never an agent). */
|
|
179
181
|
readonly ownerCss?: OwnerCssStore;
|
|
182
|
+
/** #2308: the person's own app icon; changed by a signed-in admin only (never the public link, a guest or the token). */
|
|
183
|
+
readonly appIcon?: AppIconStore;
|
|
180
184
|
/** #1629: every instance's theme (web-themes.json), read by the page; a person's reset and lock. */
|
|
181
185
|
readonly themeStore?: ThemeStore;
|
|
182
186
|
readonly webAppearance?: WebAppearance;
|
package/dist/web-api.js
CHANGED
|
@@ -5,6 +5,8 @@ import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
|
5
5
|
import { binaryProbe } from "./binary-probe.js";
|
|
6
6
|
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
7
7
|
import { readBoundedWebBody } from "./web-body.js";
|
|
8
|
+
import { APP_ICON_MAX_BYTES } from "./web-app-icon.js";
|
|
9
|
+
import { withAppIconLinks } from "./web-icons.js";
|
|
8
10
|
import { MOD_MAX_BYTES } from "./web-mod-bundle.js";
|
|
9
11
|
import { buildModBundle } from "./web-mod-export.js";
|
|
10
12
|
import { AGENT_WIDGET_SLOTS, checkWidgetData, checkWidgetSpec } from "./web-widget.js";
|
|
@@ -264,6 +266,104 @@ function stillAuthorized(req, res, url, ctx) {
|
|
|
264
266
|
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
265
267
|
return false;
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* #2308: the app icon. GET: its state, for the settings card (a signed-in person's session). PUT (one PNG, at most
|
|
271
|
+
* 5 MiB) and DELETE (back to the bundled icon): a signed-in admin's session only — never the CLI's token, a read-only
|
|
272
|
+
* session, a guest or the public link; checked again once the body has arrived.
|
|
273
|
+
*/
|
|
274
|
+
function handleAppIcon(req, res, url, ctx, method) {
|
|
275
|
+
const store = ctx.appIcon;
|
|
276
|
+
if (!store) {
|
|
277
|
+
json(res, 404, { error: "The app icon is not kept on this fleet" });
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
const reading = method === "GET" || method === "HEAD";
|
|
281
|
+
if (!reading && method !== "PUT" && method !== "DELETE") {
|
|
282
|
+
res.setHeader("Allow", "GET, PUT, DELETE");
|
|
283
|
+
json(res, 405, { error: "method not allowed" });
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (gatewayRequestContext(req)) {
|
|
287
|
+
json(res, reading ? 404 : 403, { error: "app_icon_not_over_public_link" });
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
const who = () => {
|
|
291
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
292
|
+
if (verdict.kind !== "allow")
|
|
293
|
+
return { status: 401, error: WEB_TOKEN_INVALID_MESSAGE };
|
|
294
|
+
if (verdict.via !== "session" || !verdict.session || verdict.session.tier === "guest")
|
|
295
|
+
return { status: 403, error: "app_icon_needs_a_signed_in_person" };
|
|
296
|
+
if (!reading && verdict.session.tier !== "admin")
|
|
297
|
+
return { status: 403, error: "app_icon_admin_only" };
|
|
298
|
+
return { person: webPersonKey(verdict.session.owner) };
|
|
299
|
+
};
|
|
300
|
+
const first = who();
|
|
301
|
+
if ("status" in first) {
|
|
302
|
+
json(res, first.status, { error: first.error });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
const state = () => { const s = store.current(); return s ? { custom: true, version: s.version, updatedAt: s.updatedAt } : { custom: false, version: "default", updatedAt: null }; };
|
|
306
|
+
if (reading) {
|
|
307
|
+
json(res, 200, state());
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (method === "DELETE") {
|
|
311
|
+
const reset = store.reset();
|
|
312
|
+
if (!reset.ok) {
|
|
313
|
+
json(res, reset.status, { error: reset.error, ...(reset.detail ? { detail: reset.detail } : {}) });
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
json(res, 200, state());
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
// Still allowed, still answering, the store still open: asked after each await (the body; the conversion).
|
|
320
|
+
const again = () => {
|
|
321
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
322
|
+
return null;
|
|
323
|
+
if (res.destroyed)
|
|
324
|
+
return null;
|
|
325
|
+
const now = who();
|
|
326
|
+
if ("status" in now) {
|
|
327
|
+
json(res, now.status, { error: now.error });
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return now.person;
|
|
331
|
+
};
|
|
332
|
+
(async () => {
|
|
333
|
+
let bytes;
|
|
334
|
+
try {
|
|
335
|
+
bytes = await readBoundedWebBody(req, APP_ICON_MAX_BYTES);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
json(res, 413, { error: "app_icon_too_large", detail: "at most 5 MiB" });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!again())
|
|
342
|
+
return;
|
|
343
|
+
// #2308 review: decoding and making the sizes is seconds of CPU at worst — a worker thread's, not the fleet loop's.
|
|
344
|
+
const prepared = await store.prepare(bytes);
|
|
345
|
+
if (!prepared.ok) {
|
|
346
|
+
json(res, prepared.status, { error: prepared.error, ...(prepared.detail ? { detail: prepared.detail } : {}) });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const person = again();
|
|
350
|
+
if (!person)
|
|
351
|
+
return;
|
|
352
|
+
const saved = store.commit(prepared.prepared, person);
|
|
353
|
+
if (!saved.ok) {
|
|
354
|
+
json(res, saved.status, { error: saved.error, ...(saved.detail ? { detail: saved.detail } : {}) });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
json(res, 200, state());
|
|
358
|
+
})().catch((err) => {
|
|
359
|
+
ctx.logger.error({ err: err.message }, "App icon request failed");
|
|
360
|
+
try {
|
|
361
|
+
json(res, 500, { error: "App icon request failed" });
|
|
362
|
+
}
|
|
363
|
+
catch { /* already answered */ }
|
|
364
|
+
});
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
267
367
|
/**
|
|
268
368
|
* #1629 r4 §3: the owner-CSS routes (see their call site). `who()`, asked first and again after any body: the person a
|
|
269
369
|
* signed-in human session is, or the status to answer.
|
|
@@ -1451,6 +1551,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1451
1551
|
deliveries: history ? history.deliveries() : [],
|
|
1452
1552
|
// #1266: a reply's buttons that ended after the page saw the reply (the message itself is not sent again).
|
|
1453
1553
|
reply_buttons: history ? history.buttonStates() : [],
|
|
1554
|
+
// #2305: the instance's reactions, likewise (a reaction is no new message).
|
|
1555
|
+
reactions: history ? history.reactionStates() : [],
|
|
1454
1556
|
// And the fleet prompts open on the dashboard (C4): prompt events are stream-only too.
|
|
1455
1557
|
prompts: ctx.listWebPrompts?.() ?? [],
|
|
1456
1558
|
// #1386: "Needs you" rides the passive channels only — no endpoint of its own to poll.
|
|
@@ -2103,6 +2205,9 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2103
2205
|
if (path === "/ui/owner-css.css" || path === "/ui/owner-css" || path.startsWith("/ui/owner-css/")) {
|
|
2104
2206
|
return handleOwnerCss(req, res, url, ctx, path, method);
|
|
2105
2207
|
}
|
|
2208
|
+
// #2308: the app icon (handleAppIcon: its state for a signed-in person; changing it, an admin's session only).
|
|
2209
|
+
if (path === "/ui/app-icon")
|
|
2210
|
+
return handleAppIcon(req, res, url, ctx, method);
|
|
2106
2211
|
// #1629 r4-5: mods — a person's import, uninstall and export (handleMods: sessions only, never the public link).
|
|
2107
2212
|
if (path === "/ui/mods" || path.startsWith("/ui/mods/"))
|
|
2108
2213
|
return handleMods(req, res, url, ctx, path, method);
|
|
@@ -3296,9 +3401,10 @@ function handleSendMessage(req, res, ctx) {
|
|
|
3296
3401
|
// the fleet-wide switch is off for a room that is unset) resolved at send time inside sendClassicWebEcho.
|
|
3297
3402
|
const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
|
|
3298
3403
|
const settleClassicEcho = canClassicEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
3299
|
-
const
|
|
3404
|
+
const copies = [];
|
|
3405
|
+
const posted = await ctx.sendClassicWebEcho(instance, formatWebChannelEcho(person.name, preview, t("web.echo_full_text"), { guest: !!guest }), person.name, copies);
|
|
3300
3406
|
if (posted > 0)
|
|
3301
|
-
ctx.webMessageInChannel?.(instance, messageId); // #1920 §5.2: the room saw it
|
|
3407
|
+
ctx.webMessageInChannel?.(instance, messageId, copies); // #1920 §5.2: the room saw it (#2305: its copies)
|
|
3302
3408
|
}) : undefined;
|
|
3303
3409
|
const settleEcho = canEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
3304
3410
|
// A replacement adapter or edited binding is not the route we reserved.
|
|
@@ -3307,7 +3413,11 @@ function handleSendMessage(req, res, ctx) {
|
|
|
3307
3413
|
|| ctx.fleetConfig?.instances[instance]?.topic_id !== topicId)
|
|
3308
3414
|
return;
|
|
3309
3415
|
const sent = await syncAdapter.sendText(String(groupId), formatWebChannelEcho(person.name, preview, t("web.echo_full_text"), { guest: !!guest }), { threadId: String(topicId), format: "text", allowedMentions: { parse: [] } });
|
|
3310
|
-
|
|
3416
|
+
const copyId = sent && typeof sent === "object" && typeof sent.messageId === "string" ? sent.messageId : "";
|
|
3417
|
+
// #1920 §5.2: the topic saw it. #2305: and where it is, for a reaction to the web message.
|
|
3418
|
+
const reacting = syncAdapter;
|
|
3419
|
+
ctx.webMessageInChannel?.(instance, messageId, copyId && typeof reacting.react === "function"
|
|
3420
|
+
? [{ adapter: reacting, chatId: String(groupId), threadId: String(topicId), messageId: copyId }] : []);
|
|
3311
3421
|
return sent;
|
|
3312
3422
|
}) : undefined;
|
|
3313
3423
|
let delivered;
|
|
@@ -3686,7 +3796,7 @@ export function serveAppShell(req, res, ctx, mode) {
|
|
|
3686
3796
|
const effectOrigin = gatewayRequestContext(req)?.expectedOrigin ?? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req));
|
|
3687
3797
|
const effectFrame = shellEffect(req, ctx, mode) && effectOrigin ? `${effectOrigin}/effect-bootstrap` : null;
|
|
3688
3798
|
const frameSrc = [previewFrames, effectFrame].filter(Boolean).join(" ");
|
|
3689
|
-
sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
3799
|
+
sendPanelHtml(res, withAppIconLinks(html).replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
3690
3800
|
}
|
|
3691
3801
|
catch {
|
|
3692
3802
|
json(res, 500, { error: "app.html not found" });
|