@songsid/agend 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/backend/muse.d.ts +20 -2
- package/dist/backend/muse.js +154 -16
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -1
- package/dist/channel/adapters/telegram.js +3 -35
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +2 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +155 -16
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +11 -0
- package/dist/delivery-outbox.js +47 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -1
- package/dist/fleet-lock.d.ts +6 -0
- package/dist/fleet-lock.js +19 -0
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +34 -2
- package/dist/fleet-manager.js +264 -39
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +3 -2
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -2
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +11 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +24 -2
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -0
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.js +1 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -2
- package/dist/outbound-handlers.js +5 -5
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/search/search-index.d.ts +10 -2
- package/dist/search/search-index.js +17 -4
- package/dist/search/search-index.js.map +1 -1
- package/dist/service-installer.d.ts +23 -1
- package/dist/service-installer.js +40 -11
- package/dist/service-installer.js.map +1 -1
- package/dist/session-oom-stop.d.ts +45 -0
- package/dist/session-oom-stop.js +259 -0
- package/dist/session-oom-stop.js.map +1 -0
- package/dist/session-oom.d.ts +13 -0
- package/dist/session-oom.js +119 -7
- package/dist/session-oom.js.map +1 -1
- package/dist/settings-api.d.ts +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/spawn-gate.d.ts +2 -0
- package/dist/spawn-gate.js +17 -4
- package/dist/spawn-gate.js.map +1 -1
- package/dist/start-attempt.d.ts +11 -1
- package/dist/start-attempt.js +16 -5
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +37 -0
- package/dist/start-failure-chat.js +142 -0
- package/dist/start-failure-chat.js.map +1 -0
- package/dist/start-failure.d.ts +24 -0
- package/dist/start-failure.js +68 -1
- package/dist/start-failure.js.map +1 -1
- package/dist/telegram-api-root.d.ts +1 -0
- package/dist/telegram-api-root.js +40 -0
- package/dist/telegram-api-root.js.map +1 -0
- package/dist/types.d.ts +3 -2
- package/dist/ui/panel-chat.js +19 -6
- package/dist/ui/panel-log.js +3 -3
- package/dist/ui/panel-settings.js +8 -1
- package/dist/ui/settings-strings.js +6 -0
- package/dist/ui/shared/app-shell.js +21 -4
- package/dist/ui/shared/app.css +9 -3
- package/dist/ui/shared/usage-dialog.js +21 -11
- package/dist/ui/shared/view-strings.js +2 -0
- package/dist/web-api.d.ts +1 -1
- package/dist/web-api.js +1 -1
- package/dist/web-api.js.map +1 -1
- package/dist/working-state-journal.d.ts +68 -0
- package/dist/working-state-journal.js +93 -0
- package/dist/working-state-journal.js.map +1 -0
- package/package.json +1 -1
package/dist/ui/panel-chat.js
CHANGED
|
@@ -648,7 +648,13 @@ function Composer({ name, label = name, lease, busy }) {
|
|
|
648
648
|
const [, tick] = useState(0);
|
|
649
649
|
const [sel, setSel] = useState(0);
|
|
650
650
|
const [closedFor, setClosedFor] = useState(null); // the draft the palette was closed on (Esc); it opens again on change
|
|
651
|
+
// An IME composing (Zhuyin, Pinyin, kana…): between compositionstart and compositionend the box is the IME's. Nothing
|
|
652
|
+
// here touches it — no draft update, no re-render, no auto-grow (each one moved the box, and iOS redrew the candidate
|
|
653
|
+
// bar on every keystroke: the user's flicker). What it holds is taken once, at compositionend.
|
|
654
|
+
const composing = useRef(false);
|
|
651
655
|
const value = s.drafts[name] || "";
|
|
656
|
+
// A render while composing (another part of the chat changed) gives the box what it holds, so it never writes it.
|
|
657
|
+
const boxValue = composing.current && input.current ? input.current.value : value;
|
|
652
658
|
const files = s.pendingFiles[name] || [];
|
|
653
659
|
const reply = s.replies[name]; // #2272: the message this one will reply to
|
|
654
660
|
const content = !!value.trim() || files.length > 0;
|
|
@@ -660,7 +666,7 @@ function Composer({ name, label = name, lease, busy }) {
|
|
|
660
666
|
// Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300). Empty, it is one line
|
|
661
667
|
// (#2478 — a long name's placeholder made the box two lines tall).
|
|
662
668
|
useLayoutEffect(() => {
|
|
663
|
-
const el = input.current; if (!el) return;
|
|
669
|
+
const el = input.current; if (!el || composing.current) return;
|
|
664
670
|
el.style.height = "auto";
|
|
665
671
|
if (value) el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
|
|
666
672
|
}, [value]);
|
|
@@ -700,7 +706,11 @@ function Composer({ name, label = name, lease, busy }) {
|
|
|
700
706
|
if (reply && e.key === "Escape" && !e.isComposing) { e.preventDefault(); store.clearReply(name); return; } // #2272
|
|
701
707
|
if (R().composerKey(e, { touch: touchScreen() }) === "send") { e.preventDefault(); submit(); }
|
|
702
708
|
};
|
|
703
|
-
const onInput = (e) => { setValue(e.target.value); };
|
|
709
|
+
const onInput = (e) => { if (composing.current || e.isComposing) return; setValue(e.target.value); };
|
|
710
|
+
const onCompositionStart = () => { composing.current = true; };
|
|
711
|
+
// The composed text is the draft now (some engines send no input after it); a blur ends one that never ended.
|
|
712
|
+
// Lower-case props below: an element has no oncomposition… property, so Preact would listen for "CompositionStart".
|
|
713
|
+
const onCompositionEnd = (e) => { if (!composing.current) return; composing.current = false; setValue(e.target.value); };
|
|
704
714
|
// Files in a paste are attached; a very long text paste goes as a text file — only once it is really attached.
|
|
705
715
|
const onPaste = (e) => {
|
|
706
716
|
const list = e.clipboardData && e.clipboardData.files;
|
|
@@ -727,11 +737,14 @@ function Composer({ name, label = name, lease, busy }) {
|
|
|
727
737
|
<input ref=${fileIn} type="file" multiple hidden
|
|
728
738
|
onChange=${(e) => { store.addFiles(name, e.target.files); e.target.value = ""; }} />
|
|
729
739
|
${/* #2478: the placeholder is drawn beside the box, one line ending in "…" — a textarea's own placeholder is clipped,
|
|
730
|
-
never ellipsized, and a long name wrapped it to two lines. The box keeps the words as its accessible name.
|
|
731
|
-
|
|
740
|
+
never ellipsized, and a long name wrapped it to two lines. The box keeps the words as its accessible name. Its own
|
|
741
|
+
placeholder is one space: :placeholder-shown hides the drawn one (app.css) the moment the box holds text, an
|
|
742
|
+
IME's composing text included, with no change to the page. */ ""}
|
|
743
|
+
<span class="composer-field"><textarea id="msgIn" ref=${input} rows="1" value=${boxValue} aria-label=${t("chat.composerPlaceholder", label)}
|
|
732
744
|
enterkeyhint=${touchScreen() ? "enter" : undefined} autocomplete="off" aria-controls=${palette ? "cmdPalette" : undefined} aria-expanded=${palette ? "true" : "false"}
|
|
733
|
-
aria-activedescendant=${pick ? optId(pick) : undefined} onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}
|
|
734
|
-
|
|
745
|
+
aria-activedescendant=${pick ? optId(pick) : undefined} onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}
|
|
746
|
+
oncompositionstart=${onCompositionStart} oncompositionend=${onCompositionEnd} onBlur=${onCompositionEnd}
|
|
747
|
+
placeholder=" "></textarea><span class="composer-ph" aria-hidden="true">${t("chat.composerPlaceholder", label)}</span></span>
|
|
735
748
|
<button id="stopBtn" type="button" class="btn btn-stop" hidden=${!busy} title=${t("chat.stopReplyTitle")}
|
|
736
749
|
disabled=${!!s.cancelling[name]} onClick=${() => store.cancelReply(name)}><${Icon} name="stop" size=${14} /><span class="stop-label">${t("chat.stopReply")}</span></button>
|
|
737
750
|
<button id="sendBtn" type="button" class="btn btn-primary btn-send" hidden=${busy && !content} disabled=${!content}
|
package/dist/ui/panel-log.js
CHANGED
|
@@ -62,7 +62,7 @@ register("log", {
|
|
|
62
62
|
ev_backend_unreachable: "CLI unreachable", ev_startup_backend_unreachable: "CLI unreachable at start", ev_backend_launch_warning: "Launch warning",
|
|
63
63
|
ev_clear_action: "Conversation cleared", ev_normal_exit_action: "Exited", ev_reply_drop_detected: "A reply went missing",
|
|
64
64
|
ev_reply_drop_recovered: "Missing reply recovered", ev_reply_drop_unrecovered: "Missing reply not recovered",
|
|
65
|
-
ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error",
|
|
65
|
+
ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error", ev_working_state: "Working-state journal",
|
|
66
66
|
},
|
|
67
67
|
"zh-TW": {
|
|
68
68
|
offTitle: "紀錄未開啟",
|
|
@@ -100,7 +100,7 @@ register("log", {
|
|
|
100
100
|
ev_backend_unreachable: "連不到 CLI", ev_startup_backend_unreachable: "啟動時連不到 CLI", ev_backend_launch_warning: "啟動警告",
|
|
101
101
|
ev_clear_action: "對話已清除", ev_normal_exit_action: "已結束", ev_reply_drop_detected: "有回覆遺失",
|
|
102
102
|
ev_reply_drop_recovered: "遺失的回覆已補回", ev_reply_drop_unrecovered: "遺失的回覆沒有補回",
|
|
103
|
-
ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤",
|
|
103
|
+
ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤", ev_working_state: "工作狀態紀錄",
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
106
|
const tl = (key, ...a) => t(`log.${key}`, ...a);
|
|
@@ -179,7 +179,7 @@ export function timeline(steps, events) {
|
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
const isError = (x) => x.kind === "error" || x.kind === "event" || x.status === "failed" || x.status === "interrupted";
|
|
182
|
+
const isError = (x) => x.kind === "error" || (x.kind === "event" && x.type !== "working_state") || x.status === "failed" || x.status === "interrupted";
|
|
183
183
|
/** Consecutive identical finished tool steps (same tool, subject and status) as one row with a count: a loop shows. */
|
|
184
184
|
export function collapse(items) {
|
|
185
185
|
const out = [];
|
|
@@ -811,7 +811,7 @@ function General({ ctx }) {
|
|
|
811
811
|
*/
|
|
812
812
|
export const WEB_READ_ONLY = [
|
|
813
813
|
"web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
|
|
814
|
-
"web.usage_panel", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
|
|
814
|
+
"web.usage_panel", "web.search", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
|
|
815
815
|
];
|
|
816
816
|
function WebSection({ ctx, staged }) {
|
|
817
817
|
const { schema } = ctx;
|
|
@@ -907,6 +907,12 @@ function WebSection({ ctx, staged }) {
|
|
|
907
907
|
useEffect(() => { if (!formMounted.current) { formMounted.current = true; return; } review(true); }, [f]);
|
|
908
908
|
const shown = (path) => {
|
|
909
909
|
const [top, key] = path.split(".");
|
|
910
|
+
if (top === "web" && key === "search") {
|
|
911
|
+
const days = web.search && typeof web.search === "object" ? web.search.retention_days : undefined;
|
|
912
|
+
if (days === undefined || days === null) return tn("searchRetentionUnset");
|
|
913
|
+
if (days === 0) return tn("searchRetentionZero");
|
|
914
|
+
return String(days);
|
|
915
|
+
}
|
|
910
916
|
const v = (top === "web" ? web : term)[key];
|
|
911
917
|
return v === undefined ? tn("notSet") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
912
918
|
};
|
|
@@ -958,6 +964,7 @@ function WebSection({ ctx, staged }) {
|
|
|
958
964
|
<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
|
|
959
965
|
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
960
966
|
<dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
|
|
967
|
+
<p class="note">${tn("webSearchHint")}</p>
|
|
961
968
|
</section>
|
|
962
969
|
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${() => review()}>${tn("reviewChanges")}</button>
|
|
963
970
|
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
@@ -530,6 +530,9 @@ register("settings", {
|
|
|
530
530
|
vaRevoke: "Revoke web authority", vaRevokeTitle: "Revoke all web authority?", vaRevokeConfirm: "Every authority given from the web so far reads as none from now on, and every session's authorization ends. Nobody is signed out: a session carries authority again once it is authorized again.",
|
|
531
531
|
vaRevoked: "Web authority given so far is revoked.", vaRevokedNotDurable: "Revoked for now, but it could not be saved: web authority stays off until it can be, and a restart before then undoes the revocation.", vaRevokeFailed: "That did not work — try again.",
|
|
532
532
|
webReadOnlyLead: "Read-only here: these decide what the dashboard listens on and exposes. Edit fleet.yaml, then run agend reload (web.preview_port and web.preview_origin take a restart of AgEnD).",
|
|
533
|
+
searchRetentionUnset: "not set (no age limit; 300,000-row cap)",
|
|
534
|
+
searchRetentionZero: "0 (no age limit; 300,000-row cap)",
|
|
535
|
+
webSearchHint: "Global search keeps entries with no age limit by default. The daily prune still caps the index at 300,000 rows (oldest first). Set web.search.retention_days in fleet.yaml (1–3650 days, or 0 for no age limit); it takes effect at the next fleet start.",
|
|
533
536
|
notSet: "not set (default)",
|
|
534
537
|
defaultsTitle: "Defaults",
|
|
535
538
|
classicBackend: "ClassicBot default backend",
|
|
@@ -1139,6 +1142,9 @@ register("settings", {
|
|
|
1139
1142
|
vaRevoke: "撤銷網頁授權", vaRevokeTitle: "撤銷所有網頁授權?", vaRevokeConfirm: "到目前為止從網頁給出的授權,從現在起都視為無,所有 session 的授權也會結束。不會登出任何人:session 再授權一次後,送出的訊息會再帶有授權。",
|
|
1140
1143
|
vaRevoked: "已撤銷到目前為止的網頁授權。", vaRevokedNotDurable: "已暫時撤銷,但無法保存:在可以保存之前網頁授權會保持關閉,而在那之前重新啟動會讓這次撤銷失效。", vaRevokeFailed: "沒有成功,請再試一次。",
|
|
1141
1144
|
webReadOnlyLead: "這裡僅供檢視:這些設定決定儀表板監聽和公開的內容。請編輯 fleet.yaml 後執行 agend reload(web.preview_port 與 web.preview_origin 需要重新啟動 AgEnD)。",
|
|
1145
|
+
searchRetentionUnset: "未設定(不限天數;上限 30 萬筆)",
|
|
1146
|
+
searchRetentionZero: "0(不限天數;上限 30 萬筆)",
|
|
1147
|
+
webSearchHint: "全域搜尋預設不限天數。每日清理仍會把索引上限維持在 30 萬筆(先刪最舊的)。在 fleet.yaml 設定 web.search.retention_days(1–3650 天,或 0 代表不限天數),下次啟動 fleet 後生效。",
|
|
1142
1148
|
notSet: "未設定(預設)",
|
|
1143
1149
|
defaultsTitle: "預設值",
|
|
1144
1150
|
classicBackend: "ClassicBot 預設 backend",
|
|
@@ -679,9 +679,16 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
679
679
|
// Only a touch screen (a coarse pointer) has an on-screen keyboard: a narrow desk window made shorter is not one.
|
|
680
680
|
// Turned with the keyboard up and the field still focused, the new width has no height without it yet: the keyboard
|
|
681
681
|
// stays up until the field is left, or the view grows from its lowest by more than a keyboard (put away).
|
|
682
|
-
|
|
682
|
+
// Once up, the keyboard stays up while the field keeps focus: an IME's candidate bar (or the predictive row) coming and
|
|
683
|
+
// going moves the view by tens of px on every keystroke, and must never take the tabs out and back. Only leaving the
|
|
684
|
+
// field, or the view growing from its lowest by more than a keyboard (put away), takes it down. While an IME composes
|
|
685
|
+
// nothing changes at all; what the view says then is applied at compositionend — or as soon as the field it composes
|
|
686
|
+
// in has lost focus or left the page (a composition can end without its compositionend: #2517 review).
|
|
687
|
+
let width = -1, tallest = 0, held = false, lowest = 0, upLowest = 0, composing = null;
|
|
683
688
|
const coarse = () => typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
|
|
684
689
|
const check = () => {
|
|
690
|
+
if (composing && composing.isConnected !== false && document.activeElement === composing) return;
|
|
691
|
+
composing = null;
|
|
685
692
|
const w = Math.round(vv.width), h = vv.height;
|
|
686
693
|
const typing = coarse() && isTextEntry(document.activeElement);
|
|
687
694
|
if (w !== width) { // turned, or a new width: measured again
|
|
@@ -693,14 +700,24 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
693
700
|
if (!typing || h - lowest > KEYBOARD_MIN_PX) held = false; // left the field, or the keyboard was put away
|
|
694
701
|
}
|
|
695
702
|
if (!held) tallest = Math.max(tallest, window.innerHeight || 0, typing ? 0 : h); // the view itself only when not typing
|
|
696
|
-
const
|
|
697
|
-
|
|
703
|
+
const was = shellStore.get().keyboard;
|
|
704
|
+
let up = held || keyboardUp(h, tallest, typing);
|
|
705
|
+
if (!up && was && typing && h - upLowest <= KEYBOARD_MIN_PX) up = true; // a bar's height, not the keyboard
|
|
706
|
+
upLowest = up ? (was ? Math.min(upLowest, h) : h) : 0;
|
|
707
|
+
if (up !== was) shellStore.set({ keyboard: up });
|
|
698
708
|
};
|
|
709
|
+
const start = (e) => { composing = (e && e.target) || document.activeElement || null; };
|
|
710
|
+
const end = () => { composing = null; check(); };
|
|
699
711
|
check();
|
|
700
712
|
vv.addEventListener("resize", check);
|
|
701
713
|
document.addEventListener("focusin", check);
|
|
702
714
|
document.addEventListener("focusout", check);
|
|
703
|
-
|
|
715
|
+
document.addEventListener("compositionstart", start, true);
|
|
716
|
+
document.addEventListener("compositionend", end, true);
|
|
717
|
+
return () => {
|
|
718
|
+
vv.removeEventListener("resize", check); document.removeEventListener("focusin", check); document.removeEventListener("focusout", check);
|
|
719
|
+
document.removeEventListener("compositionstart", start, true); document.removeEventListener("compositionend", end, true);
|
|
720
|
+
};
|
|
704
721
|
}, []);
|
|
705
722
|
const guest = guestPage(); // #1920: a guest's page — no operations, usage, pending changes or owner CSS
|
|
706
723
|
const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : "", guest ? "guest" : ""].filter(Boolean).join(" ");
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -791,6 +791,7 @@ img.amb-p { object-fit: contain; }
|
|
|
791
791
|
.composer-field { position: relative; flex: 1; min-width: 0; display: flex; }
|
|
792
792
|
.composer-ph { position: absolute; left: var(--s-1); right: var(--s-1); top: 7px; pointer-events: none; color: var(--text-3);
|
|
793
793
|
font-size: var(--fs-md); line-height: var(--lh-md); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
794
|
+
.composer-field textarea:not(:placeholder-shown) + .composer-ph { display: none; } /* text in the box, composing text too */
|
|
794
795
|
.btn-send { width: 36px; padding: 0; }
|
|
795
796
|
.btn-send .send-label { display: none; }
|
|
796
797
|
/* A narrow phone: Stop reply beside Send leaves the message box no room — the label is for screen readers there. */
|
|
@@ -1191,11 +1192,14 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1191
1192
|
.u-crow-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-semibold); color: var(--text); }
|
|
1192
1193
|
.u-crow .u-plan { flex: 0 100 auto; min-width: 22px; max-width: max-content; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1193
1194
|
padding-block: 0; line-height: 16px; font-size: 10.5px; }
|
|
1194
|
-
|
|
1195
|
-
.u-
|
|
1195
|
+
/* Last known numbers: a small muted "cached at …" at the name line's end (why, in its tooltip). */
|
|
1196
|
+
.u-crow-stale { margin-left: auto; flex: none; padding-left: 6px; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
1197
|
+
.u-cm { display: grid; grid-template-columns: 2.2em minmax(0, 1fr) 2.9em 3.7em; align-items: center; gap: 5px; min-width: 0; }
|
|
1198
|
+
/* Each window's own reset, after its percent: muted, tabular, right-aligned (an empty cell keeps the columns lined up). */
|
|
1199
|
+
.u-cm-reset { text-align: right; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; }
|
|
1196
1200
|
.u-cm-tag { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1197
1201
|
/* #2240: an account of several pools (agy) names each pool's line: a wider label, ending in an ellipsis if it must. */
|
|
1198
|
-
.u-crow.is-pooled .u-cm { grid-template-columns: minmax(0, 7.
|
|
1202
|
+
.u-crow.is-pooled .u-cm { grid-template-columns: minmax(0, 7.0em) minmax(0, 1fr) 2.9em 3.7em; column-gap: 7px; }
|
|
1199
1203
|
.u-cm-bar { height: 6px; border-radius: var(--r-full); background: var(--surface-2); overflow: hidden; }
|
|
1200
1204
|
.u-cm-fill { display: block; height: 100%; width: 0; min-width: 2px; border-radius: var(--r-full); background: var(--accent); }
|
|
1201
1205
|
.u-cm-fill.warn { background: var(--warn); } .u-cm-fill.crit { background: var(--danger); }
|
|
@@ -1204,6 +1208,8 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1204
1208
|
.u-crow-note { font-size: var(--fs-xs); color: var(--text-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1205
1209
|
.u-crow-note.is-alert::before { content: "●"; margin-right: 5px; color: var(--danger); }
|
|
1206
1210
|
.u-crow.is-stale .u-crow-note.is-alert::before { color: var(--warn); }
|
|
1211
|
+
/* Last known numbers in compact: as the full card shows them (#2386), muted — not current. */
|
|
1212
|
+
.u-crow.is-stale .u-cm-pct { color: var(--text-3); } .u-crow.is-stale .u-cm-fill { background: var(--text-3); }
|
|
1207
1213
|
@media (prefers-reduced-motion: no-preference) { .u-crow { transition: background-color 0.15s var(--ease); } }
|
|
1208
1214
|
.u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
|
|
1209
1215
|
@media (max-width: 1199px) { .u-rail { display: none; } }
|
|
@@ -214,18 +214,27 @@ export function compactPools(p) {
|
|
|
214
214
|
return [...pools.values()].map((x) => ({ ...x, worst: x.metrics.reduce((a, b) => ((b.used ?? 0) > (a.used ?? 0) ? b : a)) }));
|
|
215
215
|
}
|
|
216
216
|
const pctOf = (m) => Math.min(100, Math.max(0, m.used ?? 0));
|
|
217
|
+
/** When a window resets, at the clock ("14:05", "10/19 14:05"), for a tooltip; "" when unknown or already past. */
|
|
218
|
+
function resetClock(m, now = Date.now()) {
|
|
219
|
+
const at = new Date((m && m.resetsAt) || "").getTime();
|
|
220
|
+
return Number.isFinite(at) && at > now ? `↻ ${usageClock(m.resetsAt, now)}` : "";
|
|
221
|
+
}
|
|
217
222
|
/**
|
|
218
|
-
* One window on its own line: its few characters ("5h"), a full-width bar
|
|
219
|
-
*
|
|
223
|
+
* One window on its own line: its few characters ("5h"), a full-width bar, the percent coloured as the full card, and
|
|
224
|
+
* when this window resets ("↻ 8m"; the user's ask, 2026-10-23: a week's reset was hidden behind the 5 h one's). A pool's
|
|
225
|
+
* line (#2240): the pool's name in place of the window, the reset of the window its percent is, every window (with its
|
|
226
|
+
* reset time) in the tooltip.
|
|
220
227
|
*/
|
|
221
228
|
function CompactMeter({ m, tag, title }) {
|
|
222
229
|
const bar = useRef(null);
|
|
223
230
|
const pct = Math.min(100, Math.max(0, m.used ?? 0)), cls = level(pct);
|
|
224
231
|
useLayoutEffect(() => { if (bar.current) bar.current.style.width = `${pct}%`; }, [pct]); // CSSOM (#1300)
|
|
225
232
|
const word = cls === "crit" ? tn("usageNear") : cls === "warn" ? tn("usageHigh") : "";
|
|
226
|
-
|
|
233
|
+
const reset = soonestReset([m]), clock = resetClock(m);
|
|
234
|
+
return html`<span class="u-cm" title=${title || `${usageText(m.label, m.labelI18n)}: ${pct.toFixed(0)}%${word ? ` (${word})` : ""}${clock ? ` · ${clock}` : ""}`}>
|
|
227
235
|
<span class="u-cm-tag">${tag || windowTag(m)}</span><span class="u-cm-bar"><span ref=${bar} class=${`u-cm-fill ${cls}`}></span></span>
|
|
228
|
-
<span class=${`u-cm-pct ${cls}`}>${pct.toFixed(0)}%</span
|
|
236
|
+
<span class=${`u-cm-pct ${cls}`}>${pct.toFixed(0)}%</span>
|
|
237
|
+
<span class="u-cm-reset">${reset ? html`${reset.short}<span class="sr-only"> (${reset.full})</span>` : null}</span></span>`;
|
|
229
238
|
}
|
|
230
239
|
/** What a login says instead of numbers (not logged in, an error), or "" when it has numbers. */
|
|
231
240
|
function rowNote(p) {
|
|
@@ -234,26 +243,27 @@ function rowNote(p) {
|
|
|
234
243
|
return "";
|
|
235
244
|
}
|
|
236
245
|
/**
|
|
237
|
-
* A login in three short lines, under its provider's name (the user's pick, 2026-10-22): its name and plan
|
|
238
|
-
*
|
|
246
|
+
* A login in three short lines, under its provider's name (the user's pick, 2026-10-22): its name and plan; then its
|
|
247
|
+
* short window and its long one, each a labelled full-width bar, the percent and that window's own reset.
|
|
239
248
|
* Numbers not current, a failed read or no login: one muted line in place of the bars, what is wrong as its tooltip.
|
|
240
249
|
* Pressed, it opens its full card.
|
|
241
250
|
*/
|
|
242
251
|
export function CompactRow({ p, onExpand }) {
|
|
243
252
|
const title = providerTitle(p), failed = p.status === "error" || p.status === "no-credentials";
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
// Last known numbers (stale, e.g. an expired token) show as the others do (the user, 2026-10-23; the full card has since
|
|
254
|
+
// #2386), with a small "cached at 07:47" on the name line; a window that reset since is left out (compactSlots).
|
|
255
|
+
const pools = failed ? null : compactPools(p);
|
|
256
|
+
const metrics = failed ? [] : pools ? pools.flatMap((x) => x.metrics) : compactMetrics(p);
|
|
246
257
|
const note = rowNote(p), stale = p.stale ? staleNote(p) : null;
|
|
247
258
|
const say = stale ? stale.mark : note || tn("usage.no_data");
|
|
248
259
|
const detail = stale ? [stale.mark, stale.detail].filter(Boolean).join(" — ") : say;
|
|
249
|
-
const reset = soonestReset(metrics);
|
|
250
260
|
const name = title.profile || title.base;
|
|
251
261
|
return html`<button type="button" class=${`u-crow${p.stale ? " is-stale" : ""}${pools ? " is-pooled" : ""}`} aria-expanded="false" title=${`${p.name} — ${tn("usageExpand")}`} onClick=${onExpand}>
|
|
252
262
|
<span class="u-crow-head"><span class="u-crow-name">${name}</span>
|
|
253
263
|
${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
|
|
254
|
-
${
|
|
264
|
+
${stale && metrics.length ? html`<span class="u-crow-stale" title=${detail}>${p.cachedAt ? tn("usageCachedAt", { at: usageClock(p.cachedAt) }) : stale.mark}<span class="sr-only"> (${detail})</span></span>` : null}</span>
|
|
255
265
|
${pools ? pools.map((x) => html`<${CompactMeter} key=${x.pool.label} m=${x.worst} tag=${usageText(x.pool.label, x.pool.labelI18n)}
|
|
256
|
-
title=${x.metrics.map((m) => `${usageText(m.label, m.labelI18n)}: ${pctOf(m).toFixed(0)}
|
|
266
|
+
title=${x.metrics.map((m) => { const c = resetClock(m); return `${usageText(m.label, m.labelI18n)}: ${pctOf(m).toFixed(0)}%${c ? ` (${c})` : ""}`; }).join(" · ")} />`)
|
|
257
267
|
: metrics.length ? metrics.map((m, j) => html`<${CompactMeter} key=${j} m=${m} />`)
|
|
258
268
|
: html`<span class=${`u-crow-note${(stale || (p.status === "error" && !p.transient)) ? " is-alert" : ""}`} title=${detail}>${say}</span>`}
|
|
259
269
|
</button>`;
|
|
@@ -52,6 +52,7 @@ register("view", {
|
|
|
52
52
|
"usageFull": "Full",
|
|
53
53
|
"usageFullTitle": "The full card for each login",
|
|
54
54
|
"usageExpand": "Show the full card",
|
|
55
|
+
"usageCachedAt": "cached at {at}",
|
|
55
56
|
"usageCollapse": "Back to one line",
|
|
56
57
|
"usageWindowHours": "{n}h",
|
|
57
58
|
"usageWindowDays": "{n}d",
|
|
@@ -294,6 +295,7 @@ register("view", {
|
|
|
294
295
|
"usageFull": "完整",
|
|
295
296
|
"usageFullTitle": "每個登入顯示完整卡片",
|
|
296
297
|
"usageExpand": "顯示完整卡片",
|
|
298
|
+
"usageCachedAt": "快取於 {at}",
|
|
297
299
|
"usageCollapse": "收回成一行",
|
|
298
300
|
"usageWindowHours": "{n}小時",
|
|
299
301
|
"usageWindowDays": "{n}天",
|
package/dist/web-api.d.ts
CHANGED
|
@@ -212,7 +212,7 @@ export interface WebApiContext {
|
|
|
212
212
|
} | null;
|
|
213
213
|
/** A fleet.yaml instance (own key) or a ClassicBot room, now. */
|
|
214
214
|
isKnownInstance?(name: string): boolean;
|
|
215
|
-
startInstance(name: string, config: unknown, topicMode: boolean): Promise<void>;
|
|
215
|
+
startInstance(name: string, config: unknown, topicMode: boolean, kind?: "fleet-topic" | "classic", resumePaused?: boolean): Promise<void>;
|
|
216
216
|
stopInstance(name: string): Promise<{
|
|
217
217
|
backgroundWorkEnded: boolean;
|
|
218
218
|
}>;
|
package/dist/web-api.js
CHANGED
|
@@ -2625,7 +2625,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2625
2625
|
(async () => {
|
|
2626
2626
|
let held = false;
|
|
2627
2627
|
try {
|
|
2628
|
-
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
|
|
2628
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false, "fleet-topic", true));
|
|
2629
2629
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
2630
2630
|
if (!held)
|
|
2631
2631
|
json(res, 200, { started: name });
|