@songsid/agend 2.3.3-beta.5 → 2.3.3-beta.7
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/backend/antigravity.js +6 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +7 -0
- package/dist/backend/claude-code.js +14 -3
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +11 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +58 -12
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.js +2 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +2 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +22 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +37 -4
- package/dist/daemon.js.map +1 -1
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +16 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-manager.d.ts +5 -0
- package/dist/fleet-manager.js +24 -1
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +6 -0
- package/dist/instance-lifecycle.js +19 -3
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +56 -4
- package/dist/locale.js.map +1 -1
- package/dist/notify-socket.d.ts +10 -0
- package/dist/notify-socket.js +45 -0
- package/dist/notify-socket.js.map +1 -0
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quota-notice.d.ts +23 -0
- package/dist/quota-notice.js +22 -0
- package/dist/quota-notice.js.map +1 -0
- package/dist/sd-notify.d.ts +10 -1
- package/dist/sd-notify.js +27 -2
- package/dist/sd-notify.js.map +1 -1
- package/dist/settings-confirmation.d.ts +17 -3
- package/dist/settings-confirmation.js +8 -5
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-http-confirmation.js +31 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/topic-commands.d.ts +21 -5
- package/dist/topic-commands.js +93 -36
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-strings.js +6 -4
- package/dist/ui/chat-thread.js +45 -15
- package/dist/ui/fleet-office-skin.js +37 -14
- package/dist/ui/fleet-office.js +31 -7
- package/dist/ui/html-attachment.js +22 -8
- package/dist/ui/preview-panel.js +9 -6
- package/dist/ui/preview.js +10 -3
- package/dist/ui/settings-confirm.js +18 -0
- package/dist/ui/shared/app-i18n.js +13 -2
- package/dist/ui/shared/app-shell.js +35 -1
- package/dist/ui/shared/app.css +15 -0
- package/dist/web-preview.d.ts +5 -2
- package/dist/web-preview.js +6 -3
- package/dist/web-preview.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/chat-strings.js
CHANGED
|
@@ -44,7 +44,8 @@ register("chat", {
|
|
|
44
44
|
pvConfirm: "Allow HTML previews?\n\nThis applies on every device where you are signed in. A preview runs the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out, and it can slow or freeze this tab. You still click Preview each time.",
|
|
45
45
|
pvNever: "Never preview HTML on this device", pvNeverUndo: "Let this device preview HTML again", pvStarting: "Starting the preview…",
|
|
46
46
|
pvChanged: "Preview stopped — the message changed. Preview again.", pvTruncated: "Truncated — ask the agent to send it as a .html file.",
|
|
47
|
-
pvLoading: "Reading the file…", pvAttOver: "This file is over
|
|
47
|
+
pvLoading: "Reading the file…", pvAttOver: "This file is over 50 MB; it is not previewed. Download it to open it.",
|
|
48
|
+
pvLarge: "This file is large ({0}). Preview anyway?", pvAnyway: "Preview anyway",
|
|
48
49
|
pvAttGone: "This file is no longer available here (it changed, or the fleet restarted). Ask the agent to send it again.",
|
|
49
50
|
pvAttFailed: "Could not read this file. Try again.",
|
|
50
51
|
pvBannerAck: "I understand", pvBannerAckTitle: "Hide this notice until you sign in again",
|
|
@@ -64,7 +65,7 @@ register("chat", {
|
|
|
64
65
|
pvWhyNever: "Previews are switched off on this device for this session.",
|
|
65
66
|
pvWhyOrigin: "Previews are off: this page is at {0}, but the fleet believes it is at {1} (a proxy that rewrites Host). Pass the external Host through, or set web.preview_origin.",
|
|
66
67
|
pvUnknownAddress: "an unknown address", pvWhyServer: "Previews are not available here.",
|
|
67
|
-
pvHtmlOver: "This HTML is over
|
|
68
|
+
pvHtmlOver: "This HTML is over 50 MB; it is not previewed. Download it to open it.",
|
|
68
69
|
pvNoAnswer: "Preview unavailable: the preview did not answer within 3 s. If an earlier preview froze, it may still be running — open the dashboard in a new tab. Over SSH, forward the preview port too.",
|
|
69
70
|
pvStoppedAnswering: "The preview stopped answering, so it was closed.", pvSwitchedOff: "Previews were switched off on this device.",
|
|
70
71
|
pvAnother: "Another preview started.", pvWrongBoot: "Preview unavailable: something else answered on the preview port.",
|
|
@@ -167,7 +168,8 @@ register("chat", {
|
|
|
167
168
|
pvConfirm: "要允許預覽 HTML 嗎?\n\n這會套用到你登入的每一台裝置。預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
|
168
169
|
pvNever: "這台裝置永不預覽 HTML", pvNeverUndo: "讓這台裝置再次可以預覽 HTML", pvStarting: "正在啟動預覽…",
|
|
169
170
|
pvChanged: "預覽已停止——訊息內容變了。請再預覽一次。", pvTruncated: "內容被截斷——請 agent 改用 .html 檔案傳送。",
|
|
170
|
-
pvLoading: "正在讀取檔案…", pvAttOver: "這個檔案超過
|
|
171
|
+
pvLoading: "正在讀取檔案…", pvAttOver: "這個檔案超過 50 MB,不提供預覽,請下載後開啟。",
|
|
172
|
+
pvLarge: "這個檔案比較大({0}),仍要預覽嗎?", pvAnyway: "仍要預覽",
|
|
171
173
|
pvAttGone: "這裡已無法取得這個檔案(檔案已變更,或艦隊已重新啟動)。請 agent 再傳一次。",
|
|
172
174
|
pvAttFailed: "無法讀取這個檔案,請再試一次。",
|
|
173
175
|
pvBannerAck: "我知道了", pvBannerAckTitle: "隱藏這則提醒,直到下次登入",
|
|
@@ -185,7 +187,7 @@ register("chat", {
|
|
|
185
187
|
pvWhyNever: "這台裝置在這個工作階段已關閉預覽。",
|
|
186
188
|
pvWhyOrigin: "預覽已關閉:這個頁面位於 {0},但艦隊認為它位於 {1}(有會改寫 Host 的代理)。請讓外部 Host 原樣傳入,或設定 web.preview_origin。",
|
|
187
189
|
pvUnknownAddress: "未知的網址", pvWhyServer: "這裡無法使用預覽。",
|
|
188
|
-
pvHtmlOver: "這段 HTML 超過
|
|
190
|
+
pvHtmlOver: "這段 HTML 超過 50 MB,不提供預覽,請下載後開啟。",
|
|
189
191
|
pvNoAnswer: "無法預覽:預覽在 3 秒內沒有回應。如果先前的預覽卡住了,它可能還在執行——請在新分頁開啟儀表板。透過 SSH 使用時,也要轉送預覽的連接埠。",
|
|
190
192
|
pvStoppedAnswering: "預覽沒有回應,已將它關閉。", pvSwitchedOff: "這台裝置已關閉預覽。",
|
|
191
193
|
pvAnother: "另一個預覽已開始。", pvWrongBoot: "無法預覽:預覽連接埠上回應的不是這個艦隊。",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
|
|
9
9
|
import "./chat-render.js";
|
|
10
10
|
import "./preview.js";
|
|
11
|
-
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
11
|
+
import { HTML_PREVIEW_MAX, loadHtmlAttachment, sizeLabel } from "./html-attachment.js";
|
|
12
12
|
import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
|
|
13
13
|
import { textLinkSegments } from "/assets/app-route.js";
|
|
14
14
|
|
|
@@ -23,6 +23,12 @@ export const TEXT_PREVIEW_LINES = 200;
|
|
|
23
23
|
export const TEXT_PREVIEW_NODES = 4000;
|
|
24
24
|
/** A csv/tsv wider than this is shown as text, as Markdown tables are capped at 20 columns. */
|
|
25
25
|
export const TEXT_PREVIEW_COLUMNS = 50;
|
|
26
|
+
/**
|
|
27
|
+
* #2863: the most a text card reads (bytes). It stays 1 MiB while an HTML preview goes to 8 MiB (and 50 MiB on
|
|
28
|
+
* "Preview anyway"): a text card reads, parses and formats the file on this page's own thread (Markdown, highlighting,
|
|
29
|
+
* a CSV table; Show all draws every line), where an HTML preview runs in its own frame. A bigger file is a download.
|
|
30
|
+
*/
|
|
31
|
+
export const TEXT_CARD_MAX = 1024 * 1024;
|
|
26
32
|
const P = () => globalThis.AgendPreview;
|
|
27
33
|
/** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
|
|
28
34
|
export const STATUS_FOLD_LINES = 6;
|
|
@@ -406,7 +412,7 @@ export function createThread(list, scroller, opts) {
|
|
|
406
412
|
if (reading) return;
|
|
407
413
|
if (text !== null) { body.hidden = !body.hidden; show.textContent = tr(body.hidden ? "chat.tcShow" : "chat.tcHide"); return; }
|
|
408
414
|
const mine = reading = ++reads; note.textContent = tr("chat.pvLoading");
|
|
409
|
-
const r = await loadHtmlAttachment({ id: att.id, name: att.name, size: att.size });
|
|
415
|
+
const r = await loadHtmlAttachment({ id: att.id, name: att.name, size: att.size }, undefined, { ask: TEXT_CARD_MAX, max: TEXT_CARD_MAX });
|
|
410
416
|
if (reading !== mine) return; // dropped (its chat left meanwhile): a new click reads again
|
|
411
417
|
reading = 0;
|
|
412
418
|
if (!ph.isConnected) return; // the card left the page while it was read: nothing to draw
|
|
@@ -462,6 +468,8 @@ export function createThread(list, scroller, opts) {
|
|
|
462
468
|
const head = el("div", "pv-head"); head.setAttribute("data-trust", "thread-control");
|
|
463
469
|
const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
|
|
464
470
|
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
471
|
+
// #2863: a large file (over 8 MB) is previewed only on this second click, in the card or the panel as first asked.
|
|
472
|
+
const anywayB = el("button", "btn btn-sm pv-anyway", tr("chat.pvAnyway")); anywayB.type = "button"; anywayB.hidden = true;
|
|
465
473
|
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
466
474
|
// An attachment already has its download link (the file itself) right above the card.
|
|
467
475
|
const dl = att ? null : el("button", "btn btn-sm btn-ghost pv-dl", tr("chat.pvDownload"));
|
|
@@ -474,7 +482,7 @@ export function createThread(list, scroller, opts) {
|
|
|
474
482
|
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
475
483
|
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
476
484
|
menu.append(sum, optB, neverB);
|
|
477
|
-
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
485
|
+
head.append(label, run, anywayB, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
478
486
|
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
479
487
|
// #1568: the banner, with its "I understand" (hidden until the next sign-in; the frame's border stays).
|
|
480
488
|
const banner = el("div", "pv-banner"); banner.setAttribute("data-trust", "thread-control"); banner.hidden = true;
|
|
@@ -486,45 +494,64 @@ export function createThread(list, scroller, opts) {
|
|
|
486
494
|
// Reading an attachment: one read per card at a time (a second click while it reads does nothing), and a read
|
|
487
495
|
// whose card was stopped, replaced or left meanwhile is dropped (`reading` moved on, or the card left the page).
|
|
488
496
|
let reading = 0, reads = 0;
|
|
497
|
+
// #2863: where "Preview anyway" runs it ("card" | "panel") and what the card says meanwhile; null: not asked.
|
|
498
|
+
// `tooBig`: over the 50 MB ceiling (listed, or read) — the card says so and offers only the file's download.
|
|
499
|
+
let large = null, largeNote = "", tooBig = !!att && typeof att.size === "number" && att.size > HTML_PREVIEW_MAX;
|
|
489
500
|
const refresh = (reason) => {
|
|
490
501
|
const a = P().availability();
|
|
491
502
|
const going = state === "starting" || state === "running";
|
|
492
503
|
const inPanel = !!panel && panel.shown() === key; // #1481: the panel shows this block; the card points there
|
|
493
|
-
|
|
494
|
-
if (
|
|
504
|
+
if (going || reading) large = null;
|
|
505
|
+
if (reason == null && large) reason = largeNote;
|
|
506
|
+
if (reason == null && tooBig) reason = tr("chat.pvAttOver");
|
|
507
|
+
run.hidden = !a.ok || going || inPanel || !!reading || !!large || tooBig;
|
|
508
|
+
anywayB.hidden = !a.ok || !large;
|
|
509
|
+
if (toPanel) toPanel.hidden = inPanel || !!reading || tooBig;
|
|
495
510
|
stopB.hidden = !going && !reading;
|
|
496
511
|
banner.hidden = !going || P().bannerAcked();
|
|
497
512
|
note.textContent = reason != null ? reason : reading ? tr("chat.pvLoading") : going ? (state === "starting" ? tr("chat.pvStarting") : "") : inPanel ? tr("chat.pvInPanel") : a.ok ? "" : a.reason;
|
|
498
513
|
optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
|
|
499
514
|
neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
|
|
500
515
|
};
|
|
501
|
-
/**
|
|
502
|
-
const
|
|
516
|
+
/** Over 8 MB: the card asks, and "Preview anyway" runs it where `target` ("card" | "panel") says. */
|
|
517
|
+
const askLarge = (target, reason) => { large = target; largeNote = reason; refresh(reason); };
|
|
518
|
+
/**
|
|
519
|
+
* The HTML, then `then(code)`: at once for a fence or an attachment already read; otherwise read it first — a large
|
|
520
|
+
* one only once `anyway` (its listed size asks before anything is fetched).
|
|
521
|
+
*/
|
|
522
|
+
const withCode = async (then, target = "card", anyway = false) => {
|
|
503
523
|
if (code != null) { then(code); return; }
|
|
504
524
|
if (reading) return;
|
|
505
525
|
const mine = reading = ++reads;
|
|
506
526
|
refresh();
|
|
507
|
-
const r = await loadHtmlAttachment(att);
|
|
527
|
+
const r = await loadHtmlAttachment(att, undefined, { anyway });
|
|
508
528
|
if (reading !== mine || !ph.isConnected) return;
|
|
509
529
|
reading = 0;
|
|
530
|
+
if (!r.ok && r.reason === "large") { askLarge(target, trf("chat.pvLarge", sizeLabel(r.size))); return; }
|
|
531
|
+
if (!r.ok && r.reason === "over") { tooBig = true; refresh(); return; }
|
|
510
532
|
if (!r.ok) { refresh(attReason(r.reason)); return; }
|
|
511
533
|
code = r.code;
|
|
512
534
|
refresh();
|
|
513
535
|
then(code);
|
|
514
536
|
};
|
|
515
|
-
const ui = { state: (name, reason) => {
|
|
537
|
+
const ui = { state: (name, reason) => {
|
|
538
|
+
state = name === "starting" || name === "running" ? name : "idle";
|
|
539
|
+
if (name === "large") { askLarge("card", reason || ""); return; }
|
|
540
|
+
large = null; refresh(reason || null);
|
|
541
|
+
} };
|
|
516
542
|
// Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
|
|
517
543
|
// A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
|
|
518
544
|
// own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
|
|
519
545
|
// Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
|
|
520
|
-
|
|
546
|
+
const preview = (anyway) => {
|
|
521
547
|
if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
|
|
522
548
|
const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
|
|
523
549
|
withCode((c) => {
|
|
524
550
|
if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
|
|
525
|
-
P().start(key, holder, c, ui);
|
|
526
|
-
});
|
|
551
|
+
if (anyway) P().start(key, holder, c, ui, { anyway: true }); else P().start(key, holder, c, ui);
|
|
552
|
+
}, "card", anyway);
|
|
527
553
|
};
|
|
554
|
+
run.onclick = () => preview(false);
|
|
528
555
|
stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
|
|
529
556
|
// #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
|
|
530
557
|
// starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
|
|
@@ -532,15 +559,18 @@ export function createThread(list, scroller, opts) {
|
|
|
532
559
|
// An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
|
|
533
560
|
// wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
|
|
534
561
|
// a late read never undoes a newer click.
|
|
535
|
-
|
|
562
|
+
const openInPanel = (anyway) => {
|
|
536
563
|
if (reading) return; // this card already reading: that click stays the one
|
|
537
564
|
if (P().running(key)) P().stop(key, "stopped", "");
|
|
538
565
|
const at = panel.claim(), started = P().startCount();
|
|
539
566
|
withCode((c) => {
|
|
540
567
|
if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
|
|
541
|
-
|
|
542
|
-
|
|
568
|
+
large = null; refresh();
|
|
569
|
+
panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts, ...(anyway ? { anyway: true } : {}) }, { run: true });
|
|
570
|
+
}, "panel", anyway);
|
|
543
571
|
};
|
|
572
|
+
if (toPanel) toPanel.onclick = () => openInPanel(false);
|
|
573
|
+
anywayB.onclick = () => { const where = large; large = null; if (where === "panel" && toPanel) openInPanel(true); else preview(true); };
|
|
544
574
|
if (dl) dl.onclick = () => opts.download(code);
|
|
545
575
|
ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
|
|
546
576
|
optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// @property {(ctx, item, active, ms, env) => void} [drawFurniture] zones: one furniture item (active: activeWhen holds)
|
|
28
28
|
// @property {(ctx, L, env) => void} [drawForeground] zones: a pack's foreground over the whole 1280×720 scene, above
|
|
29
29
|
// the people, furniture and items in the air, below the crowd counts, name tags and bubbles
|
|
30
|
-
// @property {(ctx, x, y, look, env) => void} [drawTag] zones: a name tag under feet at (x, y)
|
|
30
|
+
// @property {(ctx, x, y, look, env, compact?) => void} [drawTag] zones: a name tag under feet at (x, y) (compact: #2860)
|
|
31
31
|
// @property {(ctx, area, n, env) => void} [drawCrowd] zones: an area's "+n" (who found no spot in it)
|
|
32
32
|
// @property {(ctx, desk, look, ms, env) => void} drawCharacter the occupant in its pose at time `ms` (0: still); in
|
|
33
33
|
// zones `desk` is the character's box at its current position (its feet at desk.seat); walking, `look.heading` is
|
|
@@ -293,21 +293,44 @@ function drawBubble(ctx, x, y, text, env, lift = 0) {
|
|
|
293
293
|
/** A zones name tag's widest text (logical units): longer names are cut with an ellipsis. */
|
|
294
294
|
export const TAG_TEXT_W = 84;
|
|
295
295
|
const TAG_FONT = "600 10px system-ui, sans-serif";
|
|
296
|
+
/**
|
|
297
|
+
* #2860: the compact tag a crowded area keeps on (it used to show tags on hover only, #2513): a smaller font, a smaller
|
|
298
|
+
* plate, a shorter cut, the same level badge.
|
|
299
|
+
*/
|
|
300
|
+
const COMPACT = { font: "500 9px system-ui, sans-serif", textW: 48, pad: 6, top: 2, h: 11 };
|
|
301
|
+
const NORMAL = { font: TAG_FONT, textW: TAG_TEXT_W, pad: 10, top: 3, h: 14 };
|
|
296
302
|
/** The box a name tag under feet at (x, y) takes (what drawTag draws; bubbles keep clear of it, #2513). */
|
|
297
|
-
export function tagBox(ctx, x, y, label) {
|
|
303
|
+
export function tagBox(ctx, x, y, label, compact = false) {
|
|
304
|
+
const k = compact ? COMPACT : NORMAL;
|
|
305
|
+
ctx.save();
|
|
306
|
+
ctx.font = k.font;
|
|
307
|
+
const t = fitText(ctx, label, k.textW), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + k.pad);
|
|
308
|
+
ctx.restore();
|
|
309
|
+
return { x: Math.round(x - w / 2), y: y + k.top, w, h: k.h, text: t };
|
|
310
|
+
}
|
|
311
|
+
/** #2860: what a tag covers with its level badge: on a full tag's top-left corner (half outside); left of a compact one. */
|
|
312
|
+
export function tagFootprint(ctx, box, badge, compact = false) {
|
|
313
|
+
if (!badge) return box;
|
|
314
|
+
if (compact) { const w = badgeWidth(ctx, badge) + 1; return { x: box.x - w, y: box.y, w: box.w + w, h: box.h }; }
|
|
315
|
+
return { x: box.x - 10, y: box.y - 7, w: box.w + 10, h: box.h + 7 };
|
|
316
|
+
}
|
|
317
|
+
function badgeWidth(ctx, badge) {
|
|
318
|
+
if (badge.mark) return 8;
|
|
298
319
|
ctx.save();
|
|
299
|
-
ctx.font =
|
|
300
|
-
const
|
|
320
|
+
ctx.font = BADGE_FONT;
|
|
321
|
+
const w = Math.max(12, Math.ceil((ctx.measureText ? ctx.measureText(badge.text).width : badge.text.length * 5) + 6));
|
|
301
322
|
ctx.restore();
|
|
302
|
-
return
|
|
323
|
+
return w;
|
|
303
324
|
}
|
|
304
325
|
/**
|
|
305
326
|
* #2661: a level badge on a name tag's top-left corner (x, y: the tag's corner), overlapping its edge like an app's
|
|
306
327
|
* badge — it takes none of the tag's width. `badge`: { text, tier } (fleet-office-levels.js badgeOf), or { mark } for a
|
|
307
328
|
* pinned title that hides the number: a small diamond.
|
|
308
329
|
*/
|
|
309
|
-
export function drawBadge(ctx, x, y, badge, pal) {
|
|
330
|
+
export function drawBadge(ctx, x, y, badge, pal, inline = false) {
|
|
310
331
|
if (!badge) return;
|
|
332
|
+
// #2860 inline (a compact tag): (x, y) is the tag's left edge and middle; the badge sits just left of it, whole.
|
|
333
|
+
if (inline) { const w = badgeWidth(ctx, badge); x = x - 1 - w + (badge.mark ? w / 2 : w / 2 - 3); y = y + (badge.mark ? 0 : 1); }
|
|
311
334
|
const c = tierColours(badge.tier, pal);
|
|
312
335
|
ctx.save();
|
|
313
336
|
ctx.globalAlpha = 1;
|
|
@@ -331,18 +354,18 @@ export function drawBadge(ctx, x, y, badge, pal) {
|
|
|
331
354
|
}
|
|
332
355
|
const BADGE_FONT = "700 8px system-ui, sans-serif";
|
|
333
356
|
|
|
334
|
-
/** Zones: a name tag under someone's feet at (x, y). */
|
|
335
|
-
function drawTag(ctx, x, y, look, env) {
|
|
357
|
+
/** Zones: a name tag under someone's feet at (x, y); `compact` (#2860): the smaller one a crowded area keeps on. */
|
|
358
|
+
function drawTag(ctx, x, y, look, env, compact = false) {
|
|
336
359
|
const { pal } = env;
|
|
360
|
+
const b = tagBox(ctx, x, y, look.label, compact);
|
|
337
361
|
ctx.save();
|
|
338
362
|
if (look.dim) ctx.globalAlpha = 0.35;
|
|
339
|
-
ctx.font = TAG_FONT;
|
|
340
|
-
|
|
341
|
-
ctx.
|
|
342
|
-
ctx.
|
|
343
|
-
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
|
|
363
|
+
ctx.font = compact ? COMPACT.font : TAG_FONT;
|
|
364
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
365
|
+
ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);
|
|
366
|
+
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(b.text, x, b.y + b.h / 2 + 0.5);
|
|
344
367
|
ctx.restore();
|
|
345
|
-
if (look.badge) drawBadge(ctx,
|
|
368
|
+
if (look.badge) compact ? drawBadge(ctx, b.x, b.y + b.h / 2, look.badge, pal, true) : drawBadge(ctx, b.x, b.y, look.badge, pal);
|
|
346
369
|
}
|
|
347
370
|
|
|
348
371
|
/**
|
package/dist/ui/fleet-office.js
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
// any part — an office names it as its style; one this page cannot load is drawn as Desks, and says so.
|
|
34
34
|
// - Crowds (#2513): an area with more people than seats packs the rest into a tighter grid, then counts who is left
|
|
35
35
|
// ("+N", in its corner, never over its name); there idle and asleep people say nothing, at most a few speak (calls
|
|
36
|
-
// first, then work), no bubble covers a name tag, and the tags
|
|
37
|
-
// (per browser) leaves the sleepers out.
|
|
36
|
+
// first, then work), no bubble covers a name tag, and the tags are compact (#2860; one that would touch another shows
|
|
37
|
+
// on hover or focus only). "Hide paused and stopped" (per browser) leaves the sleepers out.
|
|
38
38
|
// - Share… (#2636; an admin, on the dashboard reached directly or over the public link): a link anyone can open to watch an office live, read-only,
|
|
39
39
|
// until it ends (office-share-dialog.js; the page it opens is office-share-page.js).
|
|
40
40
|
// - Edit layout (an admin, a stored office on a zones skin): fleet-office-layout.js, saved as the office's overrides.
|
|
@@ -64,7 +64,7 @@ import { nodeText, stateWords } from "./fleet-org.js";
|
|
|
64
64
|
import { hits, needle } from "./fleet-canvas-layout.js";
|
|
65
65
|
import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
|
|
66
66
|
officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
|
|
67
|
-
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
|
|
67
|
+
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox, tagFootprint } from "./fleet-office-skin.js";
|
|
68
68
|
import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
|
|
69
69
|
import { actsOk, createOfficeStore } from "./fleet-office-store.js";
|
|
70
70
|
import { badgeOf, fixedOf } from "./fleet-office-levels.js";
|
|
@@ -131,6 +131,8 @@ const deviceRatio = () => Math.max(0.5, Math.min(4, Number(globalThis.devicePixe
|
|
|
131
131
|
const SIZE0 = { w: 960, h: 600 };
|
|
132
132
|
/** #2554: All offices' Fit keeps this many CSS px clear round the tiles (the zoom controls' column and a margin). */
|
|
133
133
|
const FIT_PAD = 60;
|
|
134
|
+
/** #2860: how far below its place a crowded area's compact tag drops when a neighbour's is in the way (a tag's height + 1). */
|
|
135
|
+
const COMPACT_ROW = 12;
|
|
134
136
|
/** #2554: in All offices a tile's name tags show (always) from this many CSS px a unit up; below it on hover only. */
|
|
135
137
|
export const TAG_MIN_K = 0.75;
|
|
136
138
|
const PAN_KEYS = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
|
@@ -418,13 +420,19 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
418
420
|
if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
|
|
419
421
|
// Who found no spot in an area: a count in its corner (never over its name).
|
|
420
422
|
for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
|
|
421
|
-
// In an area over capacity
|
|
423
|
+
// In an area over capacity (#2513: a crowd's full tags covered each other) everyone keeps a compact tag (#2860:
|
|
424
|
+
// in Cozy, the default, most of a fleet is in a crowd, and hover-only hid names and levels); one that would touch
|
|
425
|
+
// a tag already drawn, its badge included, is left to hover or focus. The full tags go first.
|
|
422
426
|
const over = p.L.over || new Set();
|
|
427
|
+
const crowded = (q) => over.has(q.d.area) && q.d.key !== hot && !q.look.match;
|
|
423
428
|
const tagged = (q) => !((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match);
|
|
429
|
+
const taken = [];
|
|
430
|
+
const touches = (b) => taken.some((o) => b.x < o.x + o.w && o.x < b.x + b.w && b.y < o.y + o.h && o.y < b.y + b.h);
|
|
424
431
|
for (const q of people) {
|
|
425
432
|
if (live.hidden.has(q.look.name) || !tagged(q)) continue;
|
|
426
433
|
sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
|
|
427
|
-
|
|
434
|
+
const box = tagBox(ctx, q.at.x, q.at.y, q.look.label);
|
|
435
|
+
tags.push(box); taken.push(tagFootprint(ctx, box, q.look.badge));
|
|
428
436
|
}
|
|
429
437
|
// A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
|
|
430
438
|
// #2698: an item hiding its people carries their tags too, in one row at its tag point (its own first; each keeps
|
|
@@ -434,7 +442,10 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
434
442
|
for (const f of furniture) {
|
|
435
443
|
const on = people.filter((q) => live.hidden.get(q.look.name) === f.id);
|
|
436
444
|
if (!on.length) {
|
|
437
|
-
if (!tagsOff && f.label) {
|
|
445
|
+
if (!tagsOff && f.label) {
|
|
446
|
+
const nl = npcLook(p, f), box = tagBox(ctx, f.x, f.y, nl.label);
|
|
447
|
+
sk.drawTag(ctx, f.x, f.y, nl, env); tags.push(box); taken.push(tagFootprint(ctx, box, nl.badge));
|
|
448
|
+
}
|
|
438
449
|
continue;
|
|
439
450
|
}
|
|
440
451
|
const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
|
|
@@ -447,11 +458,24 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
447
458
|
if (q ? !tagged(q) : tagsOff) continue;
|
|
448
459
|
sk.drawTag(ctx, r.x, f.y, r.look, env);
|
|
449
460
|
const box = tagBox(ctx, r.x, f.y, r.look.label);
|
|
450
|
-
tags.push(box);
|
|
461
|
+
tags.push(box); taken.push(tagFootprint(ctx, box, r.look.badge));
|
|
451
462
|
if (q) held.push({ name: q.look.name, id: f.id, box });
|
|
452
463
|
}
|
|
453
464
|
}
|
|
454
465
|
p.heldTags = held;
|
|
466
|
+
// The crowd's compact tags go last, clear of every full tag above: people's, an item's own name and the row it
|
|
467
|
+
// carries (#2862 review). A crowd's grid is tighter than a compact tag is wide (OVERFLOW.STEPS down to 32): a
|
|
468
|
+
// neighbour's tag goes a row lower (COMPACT_ROW) rather than out, so a row of people reads as two staggered rows.
|
|
469
|
+
if (!tagsOff) for (const q of people) {
|
|
470
|
+
if (live.hidden.has(q.look.name) || !crowded(q)) continue;
|
|
471
|
+
for (const dy of [0, COMPACT_ROW]) {
|
|
472
|
+
const box = tagBox(ctx, q.at.x, q.at.y + dy, q.look.label, true), foot = tagFootprint(ctx, box, q.look.badge, true);
|
|
473
|
+
if (touches(foot)) continue;
|
|
474
|
+
sk.drawTag(ctx, q.at.x, q.at.y + dy, q.look, env, true);
|
|
475
|
+
tags.push(box); taken.push(foot);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
455
479
|
} else {
|
|
456
480
|
for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, actPosed(p, look), ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12, area: p.roomOf.get(d.key) ?? null }); }
|
|
457
481
|
blit(p.layers.front);
|
|
@@ -3,24 +3,38 @@
|
|
|
3
3
|
// card. What runs is then handed to the preview frame exactly like a block's (AgendPreview.start): the file URL itself
|
|
4
4
|
// is never opened or framed on the dashboard's origin.
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* #2863: the sizes a preview takes (UTF-8 bytes of the HTML), the same as a block's (preview.js LIMITS): up to ASK it
|
|
8
|
+
* runs at once; above it the person is asked first ("Preview anyway?"); above MAX there is no preview, only the
|
|
9
|
+
* download. MAX is the frame's own ceiling (web-preview.ts PREVIEW_HTML_MAX_BYTES).
|
|
10
|
+
*/
|
|
11
|
+
export const HTML_PREVIEW_ASK = 8 * 1024 * 1024;
|
|
12
|
+
export const HTML_PREVIEW_MAX = 50 * 1024 * 1024;
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
|
-
* Read an attachment's HTML: { ok: true, code } or { ok: false, reason } with reason "
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* Read an attachment's HTML: { ok: true, code } or { ok: false, reason } with reason "large" (above `ask` and not
|
|
16
|
+
* `anyway`: nothing is fetched when its listed size says so; `size` is what it is), "over" (above `max`: its listed size
|
|
17
|
+
* is checked before anything is fetched, the bytes again after), "gone" (the fleet no longer serves that id — the file
|
|
18
|
+
* changed, or the fleet restarted), or "failed". The text card reads with its own, single cap (ask = max).
|
|
13
19
|
*/
|
|
14
|
-
export async function loadHtmlAttachment(att, fetchImpl = globalThis.fetch) {
|
|
20
|
+
export async function loadHtmlAttachment(att, fetchImpl = globalThis.fetch, { ask = HTML_PREVIEW_ASK, max = HTML_PREVIEW_MAX, anyway = false } = {}) {
|
|
15
21
|
if (!att || typeof att.id !== "string" || !/^[0-9a-f]{32}$/.test(att.id)) return { ok: false, reason: "failed" };
|
|
16
|
-
if (typeof att.size === "number" && att.size >
|
|
22
|
+
if (typeof att.size === "number" && att.size > max) return { ok: false, reason: "over" };
|
|
23
|
+
if (typeof att.size === "number" && att.size > ask && !anyway) return { ok: false, reason: "large", size: att.size };
|
|
17
24
|
let res;
|
|
18
25
|
try { res = await fetchImpl(`/ui/file/${att.id}`, { credentials: "same-origin", cache: "no-store" }); }
|
|
19
26
|
catch { return { ok: false, reason: "failed" }; }
|
|
20
27
|
if (!res || !res.ok) return { ok: false, reason: res && res.status === 404 ? "gone" : "failed" };
|
|
21
28
|
let bytes;
|
|
22
29
|
try { bytes = await res.arrayBuffer(); } catch { return { ok: false, reason: "failed" }; }
|
|
23
|
-
if (bytes.byteLength >
|
|
30
|
+
if (bytes.byteLength > max) return { ok: false, reason: "over" };
|
|
31
|
+
if (bytes.byteLength > ask && !anyway) return { ok: false, reason: "large", size: bytes.byteLength };
|
|
24
32
|
// UTF-8, as a block's text is; a BOM is dropped.
|
|
25
33
|
return { ok: true, code: new TextDecoder("utf-8").decode(bytes) };
|
|
26
34
|
}
|
|
35
|
+
|
|
36
|
+
/** #2863: a size as the person reads it: "12.3 MB" (1 MB = 1024 × 1024 bytes, as the caps are). */
|
|
37
|
+
export function sizeLabel(bytes) {
|
|
38
|
+
const mb = bytes / (1024 * 1024);
|
|
39
|
+
return `${mb >= 10 ? Math.round(mb) : Math.round(mb * 10) / 10} MB`;
|
|
40
|
+
}
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -152,11 +152,12 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
152
152
|
const [, dayTick] = useState(0);
|
|
153
153
|
useEffect(() => (open ? onDayChange(() => dayTick(n => n + 1)) : undefined), [!!open]);
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
const
|
|
155
|
+
// #2863: "large" — over 8 MB, nothing runs until "Preview anyway" (or the card's, which opened it with `anyway`).
|
|
156
|
+
const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" || s === "large" ? s : "idle", reason: reason || "" }) };
|
|
157
|
+
const start = (anyway) => {
|
|
157
158
|
if (!open || !holder.current) return;
|
|
158
159
|
setView("preview");
|
|
159
|
-
P().start(frameKey(open.key), holder.current, open.code, ui, { fill: true });
|
|
160
|
+
P().start(frameKey(open.key), holder.current, open.code, ui, anyway === true || open.anyway ? { fill: true, anyway: true } : { fill: true });
|
|
160
161
|
};
|
|
161
162
|
const stop = () => { if (open) P().stop(frameKey(open.key), "stopped", ""); };
|
|
162
163
|
// A new block in the panel: what ran before is gone (openPanel stopped it); the card's click starts it here — or,
|
|
@@ -224,7 +225,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
224
225
|
if (newer.code != null) { openPanel(newer); return; }
|
|
225
226
|
const was = open && open.key, mine = reading.current = newer;
|
|
226
227
|
setNewerNote(t("chat.pvLoading"));
|
|
227
|
-
|
|
228
|
+
// A click on the newer version reads it whatever its size (up to the 50 MB ceiling); over 8 MB the panel then asks.
|
|
229
|
+
const r = await loadHtmlAttachment(newer.att, undefined, { anyway: true });
|
|
228
230
|
if (reading.current !== mine) return;
|
|
229
231
|
reading.current = null;
|
|
230
232
|
const now = panelStore.get().open;
|
|
@@ -270,9 +272,10 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
270
272
|
<button type="button" class=${`btn btn-sm${view === "code" ? " on" : ""}`} aria-pressed=${view === "code" ? "true" : "false"} onClick=${() => setView("code")}><${Icon} name="code" size=${14} />${t("chat.pvCode")}</button>
|
|
271
273
|
</div>
|
|
272
274
|
<span class="grow"></span>
|
|
273
|
-
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
275
|
+
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(true); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
274
276
|
<button type="button" class="btn btn-sm pv-panel-stop" title=${t("chat.pvStop")} onClick=${stop}><${Icon} name="stop" size=${14} /><span class="lbl">${t("chat.pvStop")}</span></button>`
|
|
275
|
-
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-
|
|
277
|
+
: run.state === "large" ? html`<button type="button" class="btn btn-sm btn-primary pv-panel-anyway" title=${t("chat.pvAnyway")} disabled=${!a.ok} onClick=${() => start(true)}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvAnyway")}</span></button>`
|
|
278
|
+
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-run" title=${t("chat.pvPreview")} disabled=${!a.ok} onClick=${() => start()}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvPreview")}</span></button>`}
|
|
276
279
|
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code, fileName)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
277
280
|
</div>
|
|
278
281
|
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
package/dist/ui/preview.js
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
})(typeof globalThis !== "undefined" ? globalThis : this, function (root) {
|
|
21
21
|
"use strict";
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// #2863: askBytes — above it the person is asked first ("Preview anyway?"); maxBytes — above it only a download (the
|
|
24
|
+
// frame's own ceiling, web-preview.ts PREVIEW_HTML_MAX_BYTES; html-attachment.js reads with the same two).
|
|
25
|
+
var LIMITS = { askBytes: 8 * 1024 * 1024, maxBytes: 50 * 1024 * 1024, minHeight: 40, maxHeight: 4000, readyMs: 3000, watchdogMs: 10000, resizePerSec: 10, minDelta: 2, growthFreeze: 5, growthWindowMs: 2000 };
|
|
24
26
|
var OPT_IN_KEY = "agend_html_preview"; // localStorage: "on" — this device allows previews
|
|
25
27
|
var NEVER_KEY = "agend_html_preview_never"; // sessionStorage: "1" — never preview on this device (this session)
|
|
26
28
|
// #1568: the banner was acknowledged ("I understand"). localStorage: the sign-in's session handle — hidden while the
|
|
@@ -54,6 +56,8 @@
|
|
|
54
56
|
function now() { return root.performance && typeof root.performance.now === "function" ? root.performance.now() : Date.now(); }
|
|
55
57
|
function store(kind) { try { return root[kind] || null; } catch (e) { return null; } }
|
|
56
58
|
function utf8Bytes(s) { try { return new root.TextEncoder().encode(s).length; } catch (e) { return String(s).length * 3; } }
|
|
59
|
+
/** #2863: "12 MB" / "8.4 MB" (1 MB = 1024 × 1024 bytes, as the caps are; html-attachment.js sizeLabel). */
|
|
60
|
+
function sizeLabel(bytes) { var mb = bytes / (1024 * 1024); return (mb >= 10 ? Math.round(mb) : Math.round(mb * 10) / 10) + " MB"; }
|
|
57
61
|
function randomCh() {
|
|
58
62
|
var b = new Uint8Array(16);
|
|
59
63
|
root.crypto.getRandomValues(b);
|
|
@@ -229,7 +233,8 @@
|
|
|
229
233
|
|
|
230
234
|
/**
|
|
231
235
|
* Run `html` in `holder` (a node of the card) for `key`. `ui` is told what happened: ui.state(name, reason) with
|
|
232
|
-
* "starting" | "running" | "stopped" | "unavailable"
|
|
236
|
+
* "starting" | "running" | "stopped" | "unavailable" | "large" (#2863: over LIMITS.askBytes — nothing runs until it is
|
|
237
|
+
* asked again with opts.anyway, the person's "Preview anyway"). One preview at a time: any other running one stops first.
|
|
233
238
|
* opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
|
|
234
239
|
* panel instead of by the frame's height messages, which it then ignores.
|
|
235
240
|
*/
|
|
@@ -239,7 +244,9 @@
|
|
|
239
244
|
function start(key, holder, html, ui, opts) {
|
|
240
245
|
var a = availability();
|
|
241
246
|
if (!a.ok) { ui.state("unavailable", a.reason); return false; }
|
|
242
|
-
|
|
247
|
+
var bytes = typeof html === "string" ? utf8Bytes(html) : Infinity;
|
|
248
|
+
if (bytes > LIMITS.maxBytes) { ui.state("unavailable", say("pvHtmlOver", "This HTML is over 50 MB; it is not previewed. Download it to open it.")); return false; }
|
|
249
|
+
if (bytes > LIMITS.askBytes && !(opts && opts.anyway)) { ui.state("large", say("pvLarge", "This file is large ({0}). Preview anyway?", sizeLabel(bytes))); return false; }
|
|
243
250
|
stopAll("another");
|
|
244
251
|
starts++;
|
|
245
252
|
var doc = holder.ownerDocument;
|
|
@@ -65,6 +65,8 @@ function publish() {
|
|
|
65
65
|
// Monotonic: when the server's countdown reaches zero on this page's clock.
|
|
66
66
|
deadline: at + Math.max(0, Number(view.remaining_ms) || 0),
|
|
67
67
|
withdraw: view.can_withdraw && view.state === "pending" ? () => withdraw(view.id) : null,
|
|
68
|
+
// #2868: this browser may confirm it itself now (the server's read says so; it checks again on the click).
|
|
69
|
+
applyNow: view.can_self_confirm && view.state === "pending" ? () => applyNow(view.id) : null,
|
|
68
70
|
dismiss: TERMINAL.has(view.state) ? () => forget(view.id) : null,
|
|
69
71
|
})),
|
|
70
72
|
});
|
|
@@ -150,6 +152,22 @@ export async function withdraw(id) {
|
|
|
150
152
|
return false;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/**
|
|
156
|
+
* #2868: "Apply now" — confirm this session's own pending request from this browser (#2170's rule, asked again by the
|
|
157
|
+
* server). Its answer is followed like any other; a refusal (no longer allowed here) leaves it waiting and is { ok: false }.
|
|
158
|
+
*/
|
|
159
|
+
export async function applyNow(id) {
|
|
160
|
+
const e = tracked.get(id);
|
|
161
|
+
if (!e) return { ok: false };
|
|
162
|
+
const token = ticket(e);
|
|
163
|
+
let res;
|
|
164
|
+
try { res = await api(`/api/settings/pending/${encodeURIComponent(id)}/confirm`, { method: "POST", body: "{}" }); } catch { return { ok: false }; }
|
|
165
|
+
if (isPendingResponse(res) && res.body.pending_change.id === id) { update(res.body.pending_change, token); schedule(); return { ok: true }; }
|
|
166
|
+
// Refused (403: not allowed here any more): read it again, so "Apply now" goes until the server offers it again.
|
|
167
|
+
if (e.view.can_self_confirm) update({ ...e.view, can_self_confirm: false }, token);
|
|
168
|
+
return { ok: false, status: res.status };
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
/** This session's requests still open on the server (after a reload): followed like new ones. */
|
|
154
172
|
export async function attach() {
|
|
155
173
|
let res;
|
|
@@ -89,7 +89,13 @@ register("app", {
|
|
|
89
89
|
pendingWeb: "Confirmed by your account (this browser is web-verified). General is told.",
|
|
90
90
|
pending_applied: "Confirmed and applied", pending_rejected: "Not applied", pending_expired: "Expired — nothing was changed",
|
|
91
91
|
pending_stale: "Not applied — settings changed meanwhile", pending_failed: "Confirmed, but applying it failed",
|
|
92
|
-
|
|
92
|
+
pendingLeft: "{0} left", withdraw: "Withdraw",
|
|
93
|
+
// #2868: where to confirm it, said plainly; the host's command folded away unless it is the only way.
|
|
94
|
+
pendingPosting: "Sending the confirm card to General…", pendingChatWhere: "To apply it, press Confirm on the card in General on {0}.",
|
|
95
|
+
pendingHostOnly: "No General can take the confirm card. Confirm it on the host: {0}", pendingAdvanced: "Advanced: confirm on the host",
|
|
96
|
+
pendingSelf: "This browser is verified, so you can also apply it here.", pendingApplyNow: "Apply now",
|
|
97
|
+
pendingApplyRefused: "This browser can't apply it here now — confirm it in General.",
|
|
98
|
+
pendingAuthorize: "Authorize this browser to apply changes like this here next time, without General.",
|
|
93
99
|
opWriting: "Applying settings… {0}/{1}", opWaiting: "Settings: waiting for confirmation in chat", opApplying: "Applying settings…",
|
|
94
100
|
opDone: "Settings applied", opRestart: "Settings saved — AgEnD needs a restart to use them", opPartial: "Some settings were not applied",
|
|
95
101
|
opFailed: "Settings were not applied", openSettings: "Open Settings",
|
|
@@ -131,7 +137,12 @@ register("app", {
|
|
|
131
137
|
pendingWeb: "由你的帳號確認(這個瀏覽器已通過網頁驗證),也會通知 General。",
|
|
132
138
|
pending_applied: "已確認並套用", pending_rejected: "未套用", pending_expired: "已逾時——沒有任何變更",
|
|
133
139
|
pending_stale: "未套用——設定在這期間已變動", pending_failed: "已確認,但套用失敗",
|
|
134
|
-
|
|
140
|
+
pendingLeft: "剩 {0}", withdraw: "撤回",
|
|
141
|
+
pendingPosting: "正在把確認卡送到 General…", pendingChatWhere: "要套用,請到 {0} 的 General,在確認卡上按「確認」。",
|
|
142
|
+
pendingHostOnly: "沒有可以接收確認卡的 General。請在主機上確認:{0}", pendingAdvanced: "進階:在主機上確認",
|
|
143
|
+
pendingSelf: "這個瀏覽器已通過驗證,也可以直接在這裡套用。", pendingApplyNow: "直接套用",
|
|
144
|
+
pendingApplyRefused: "這個瀏覽器現在無法直接套用,請到 General 確認。",
|
|
145
|
+
pendingAuthorize: "授權這個瀏覽器,之後這類變更可以直接套用,不必到 General。",
|
|
135
146
|
opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
|
|
136
147
|
opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
|
|
137
148
|
opFailed: "設定沒有套用", openSettings: "開啟設定",
|