@songsid/agend 2.3.1 → 2.3.2-beta.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/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/config-validator.js +40 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +34 -3
- package/dist/daemon.js +227 -46
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +12 -1
- package/dist/delivery-outbox.js +67 -15
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-manager.d.ts +140 -0
- package/dist/fleet-manager.js +414 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +106 -55
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +107 -56
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +16 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +38 -10
- package/dist/locale.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.d.ts +26 -0
- package/dist/settings-api.js +107 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/start-attempt.d.ts +9 -5
- package/dist/start-attempt.js +27 -8
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +4 -0
- package/dist/start-failure-chat.js +23 -6
- package/dist/start-failure-chat.js.map +1 -1
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +25 -0
- package/dist/start-failure.js +75 -2
- package/dist/start-failure.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +130 -21
- package/dist/ui/fleet-office-store.js +75 -4
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +530 -94
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +578 -163
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +102 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +59 -4
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +242 -17
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +8 -2
- package/dist/web-api.js +160 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +3 -1
- package/dist/working-state-journal.js +15 -2
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #1939 S2a: Settings → Web → Web authority — whether a signed-in admin's web messages carry their account's authority
|
|
2
2
|
// in delivery receipts (docs/design/1939-verified-web-sessions.md §0, §0.1, §3, §10): the server switch (on by default),
|
|
3
3
|
// "Only this computer can authorize", how long a session's authorization lasts, the sessions authorized now, and "Revoke
|
|
4
|
-
// web authority".
|
|
4
|
+
// web authority". #2347: an edit is unsaved at once and saved by the Settings bar's Save like every Settings edit, and applies at once (§0.1: none of
|
|
5
5
|
// them grants anything by itself — a session still needs its account's Confirm in Discord or Telegram).
|
|
6
6
|
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
7
7
|
import { t } from "/assets/app-i18n.js";
|
|
@@ -9,8 +9,9 @@ import { useTypedText } from "/assets/text-undo.js";
|
|
|
9
9
|
import { toast } from "/assets/ui-toast.js";
|
|
10
10
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
11
11
|
import { api } from "./settings-confirm.js";
|
|
12
|
-
import { Feedback } from "./settings-dialogs.js";
|
|
13
|
-
import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
|
|
12
|
+
import { Feedback, InEffect } from "./settings-dialogs.js";
|
|
13
|
+
import { DEFAULT_SCHEMA, impactOf, sameValue } from "./settings-model.js";
|
|
14
|
+
import { editsOf, keepDraft, syncStage } from "./settings-draft.js";
|
|
14
15
|
import { resetWebAuthorization } from "/assets/web-authorize.js";
|
|
15
16
|
import "./settings-strings.js";
|
|
16
17
|
|
|
@@ -24,12 +25,14 @@ export function verifiedAuthorityBody(original, f) {
|
|
|
24
25
|
if (f.hours !== original.hours) out.max_session_hours = f.hours === null ? null : f.hours;
|
|
25
26
|
// #2453 (§0.2): off by default; turning it on is confirmed in a General or on the host.
|
|
26
27
|
if (f.namedLink !== original.namedLink) out.named_link = f.namedLink ? true : null;
|
|
28
|
+
// §0.3: off by default (the user's decision); turning it on is confirmed in a General or on the host.
|
|
29
|
+
if (f.quickLink !== original.quickLink) out.quick_link = f.quickLink ? true : null;
|
|
27
30
|
return Object.keys(out).length ? out : null;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
31
34
|
const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
|
|
32
|
-
const [original, setOriginal] = useState(null), [f, setF] = useState(null)
|
|
35
|
+
const [original, setOriginal] = useState(null), [f, setF] = useState(null);
|
|
33
36
|
// #2342: the hours box keeps what was typed ("01", "1.50"; its undo history); the form gets the number.
|
|
34
37
|
const [hoursText, onHoursText] = useTypedText(f ? f.hours : null, (v) => (v.trim() === "" ? null : Number(v)));
|
|
35
38
|
const load = async () => {
|
|
@@ -42,12 +45,24 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
42
45
|
if (!o || typeof o !== "object" || !o.state || !o.hours || !Array.isArray(o.lanHosts) || !Array.isArray(o.authorized)) { setFailed(true); return; }
|
|
43
46
|
const c = o.configured || {};
|
|
44
47
|
const orig = { enabled: c.enabled !== false, loopbackOnly: c.loopback_only === true, hours: typeof c.max_session_hours === "number" ? c.max_session_hours : null,
|
|
45
|
-
namedLink: c.named_link === true };
|
|
48
|
+
namedLink: c.named_link === true, quickLink: c.quick_link === true };
|
|
46
49
|
setOverview(o); setFailed(false);
|
|
47
50
|
setOriginal((prev) => prev || orig);
|
|
48
|
-
|
|
51
|
+
// #2347: a draft kept from before this card was last shown (still unsaved) comes back over what is saved now — only
|
|
52
|
+
// its edited fields (#2367 review: a field never touched follows the configuration, another admin's change too).
|
|
53
|
+
setF((prev) => prev || { ...orig, ...((ctx.drafts && ctx.drafts.get("verified_authority")) || {}) });
|
|
49
54
|
};
|
|
50
55
|
useEffect(() => { load(); }, []);
|
|
56
|
+
// #2347: every edit is unsaved at once — kept as the draft and staged (an invalid hours box: Save waits for it).
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!f || !original || !overview || disabled) return;
|
|
59
|
+
keepDraft(ctx, "verified_authority", editsOf(f, original), f);
|
|
60
|
+
const bad = f.hours !== null && (!Number.isFinite(f.hours) || f.hours < overview.hours.min || f.hours > overview.hours.max);
|
|
61
|
+
if (bad) { syncStage(ctx, "verified_authority", { label: tn("vaTitle"), invalid: tn("vaHoursRange"), section: "access", target: "#vaHours" }); return; }
|
|
62
|
+
const body = verifiedAuthorityBody(original, f);
|
|
63
|
+
syncStage(ctx, "verified_authority", body ? { label: tn("vaTitle"), count: Object.keys(body).length,
|
|
64
|
+
impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.verified_authority"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } } : null);
|
|
65
|
+
}, [f, original, overview]);
|
|
51
66
|
const card = (body) => html`<section class="card s-web-authority"><h3>${tn("vaTitle")}</h3><p class="note">${tn("vaHint")}</p>${body}</section>`;
|
|
52
67
|
if (failed) return card(html`<p class="note" role="status">${tn("vaLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`);
|
|
53
68
|
if (!overview || !f) return card(html`<p class="note">${t("app.loading")}</p>`);
|
|
@@ -58,13 +73,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
58
73
|
const warnHosts = !f.loopbackOnly && overview.lanHosts.length > 0;
|
|
59
74
|
const when = (ms) => new Date(ms).toLocaleString();
|
|
60
75
|
|
|
61
|
-
const
|
|
62
|
-
if (hoursError) return;
|
|
63
|
-
const body = verifiedAuthorityBody(original, f);
|
|
64
|
-
if (body) ctx.stage("verified_authority", { label: tn("vaTitle"), impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.verified_authority"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } });
|
|
65
|
-
else if (staged.has("verified_authority")) ctx.unstage("verified_authority");
|
|
66
|
-
setMsg(tn("readyApply"));
|
|
67
|
-
};
|
|
76
|
+
const fc = (cls, key) => `${cls}${!sameValue(f[key], original[key]) ? " s-dirty" : ""}`;
|
|
68
77
|
const revoke = async () => {
|
|
69
78
|
const yes = await confirmDialog({ title: tn("vaRevokeTitle"), message: tn("vaRevokeConfirm"), confirmLabel: tn("vaRevoke"), danger: true });
|
|
70
79
|
if (!yes) return;
|
|
@@ -76,24 +85,26 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
76
85
|
|
|
77
86
|
return card(html`<fieldset class="s-va-fields" disabled=${disabled}>
|
|
78
87
|
${stuckOff ? html`<p class="note s-va-warn" role="status" id="vaStuckOff">${tn("vaStuckOff")}</p>` : null}
|
|
79
|
-
<label class
|
|
88
|
+
<label class=${fc("check", "enabled")}><input id="vaEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("vaEnabled")}</span></label>
|
|
80
89
|
<p class="note">${tn("vaEnabledHint")}</p>
|
|
81
|
-
<label class
|
|
90
|
+
<label class=${fc("check", "loopbackOnly")}><input id="vaLoopbackOnly" type="checkbox" checked=${f.loopbackOnly} onChange=${(e) => set({ loopbackOnly: e.target.checked })} /><span>${tn("vaLoopbackOnly")}</span></label>
|
|
82
91
|
<p class="note">${tn("vaLoopbackOnlyHint")}</p>
|
|
83
92
|
${warnHosts ? html`<p class="note s-va-warn" role="status" id="vaLanWarning">${tn("vaLanWarning", overview.lanHosts.join(", "))}</p>` : null}
|
|
84
|
-
${overview.namedHost || original.namedLink ? html`<label class
|
|
93
|
+
${overview.namedHost || original.namedLink ? html`<label class=${fc("check", "namedLink")}><input id="vaNamedLink" type="checkbox" checked=${f.namedLink} onChange=${(e) => set({ namedLink: e.target.checked })} /><span>${tn("vaNamedLink", overview.namedHost || "—")}</span></label>
|
|
85
94
|
<p class="note">${tn("vaNamedLinkHint")}</p>` : null}
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
${/* §0.3: always shown — any fleet can open the temporary public link with /web. */ ""}
|
|
96
|
+
<label class=${fc("check", "quickLink")}><input id="vaQuickLink" type="checkbox" checked=${f.quickLink} onChange=${(e) => set({ quickLink: e.target.checked })} /><span>${tn("vaQuickLink")}</span></label>
|
|
97
|
+
<p class="note">${tn("vaQuickLinkHint")}</p>
|
|
98
|
+
<div class=${fc("field", "hours")}><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
|
|
99
|
+
<input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any"
|
|
88
100
|
class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: onHoursText(e.target.value) })} />
|
|
89
|
-
<${Feedback} error=${hoursError}
|
|
101
|
+
<${Feedback} error=${hoursError} />
|
|
102
|
+
${hoursText.trim() === "" ? html`<${InEffect} text=${tn("hoursValue", overview.hours.default)} />` : null}</div>
|
|
90
103
|
<h4>${tn("vaSessionsTitle")}</h4>
|
|
91
104
|
${overview.authorized.length
|
|
92
105
|
? html`<ul class="s-va-sessions" id="vaSessions">${overview.authorized.map((a) => html`<li key=${a.handle} data-handle=${a.handle}>
|
|
93
|
-
<strong>${a.label}</strong> <span class="note">${a.account}</span>${a.named ? html` <span class="note s-va-named">${tn("vaSessionNamed")}</span>` : null}<br /><span class="note">${tn("vaSessionUntil", when(a.authorizedAt), when(a.until))}</span></li>`)}</ul>`
|
|
106
|
+
<strong>${a.label}</strong> <span class="note">${a.account}</span>${a.named ? html` <span class="note s-va-named">${tn("vaSessionNamed")}</span>` : null}${a.quick ? html` <span class="note s-va-named s-va-quick">${tn("vaSessionQuick")}</span>` : null}<br /><span class="note">${tn("vaSessionUntil", when(a.authorizedAt), when(a.until))}</span></li>`)}</ul>`
|
|
94
107
|
: html`<p class="note" id="vaSessionsNone">${tn("vaSessionsNone")}</p>`}
|
|
95
108
|
</fieldset>
|
|
96
|
-
<div class="save-row"><button type="button" class="btn btn-
|
|
97
|
-
<button type="button" class="btn btn-danger" id="vaRevoke" disabled=${disabled} onClick=${revoke}>${tn("vaRevoke")}</button>
|
|
98
|
-
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>`);
|
|
109
|
+
<div class="save-row"><button type="button" class="btn btn-danger" id="vaRevoke" disabled=${disabled} onClick=${revoke}>${tn("vaRevoke")}</button></div>`);
|
|
99
110
|
}
|
|
@@ -51,6 +51,12 @@ export function setLang(next, opts = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
export function onLang(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
53
53
|
|
|
54
|
+
/** #2347: a string in a given language (settings search matches the labels of both), English when it has none. */
|
|
55
|
+
export function tIn(language, key) {
|
|
56
|
+
const table = tables[language] || tables.en;
|
|
57
|
+
return table[key] ?? tables.en[key] ?? key;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
/** "{0} is working…" with its values filled in. Text only: callers render it as text, never as markup. */
|
|
55
61
|
export function t(key, ...values) {
|
|
56
62
|
let s = tables[current][key] ?? tables.en[key] ?? key;
|
|
@@ -87,6 +93,10 @@ register("app", {
|
|
|
87
93
|
opWriting: "Applying settings… {0}/{1}", opWaiting: "Settings: waiting for confirmation in chat", opApplying: "Applying settings…",
|
|
88
94
|
opDone: "Settings applied", opRestart: "Settings saved — AgEnD needs a restart to use them", opPartial: "Some settings were not applied",
|
|
89
95
|
opFailed: "Settings were not applied", openSettings: "Open Settings",
|
|
96
|
+
restartWaiting: "{0} saved change(s) take effect after AgEnD restarts", restartNow: "Restart now", restartLater: "Later",
|
|
97
|
+
restartConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
98
|
+
restartOnHost: "Run agend restart on the host to use them.",
|
|
99
|
+
restartRefused: "Restart refused: {0}",
|
|
90
100
|
pendingSent: "Sent for confirmation — a fleet admin confirms it in chat.",
|
|
91
101
|
pendingSelfSent: "Confirmed by your account (this browser is web-verified) — applying.",
|
|
92
102
|
needsNav: "Needs you", needsTab: "Needs you", needsCount: "{0} waiting",
|
|
@@ -124,6 +134,10 @@ register("app", {
|
|
|
124
134
|
opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
|
|
125
135
|
opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
|
|
126
136
|
opFailed: "設定沒有套用", openSettings: "開啟設定",
|
|
137
|
+
restartWaiting: "{0} 項已儲存的變更要在 AgEnD 重新啟動後才會生效", restartNow: "立即重新啟動", restartLater: "稍後",
|
|
138
|
+
restartConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
139
|
+
restartOnHost: "請在主機上執行 agend restart 讓它們生效。",
|
|
140
|
+
restartRefused: "重新啟動被拒絕:{0}",
|
|
127
141
|
pendingSent: "已送出確認——由 fleet 管理員在聊天中確認。",
|
|
128
142
|
pendingSelfSent: "已由你的帳號確認(這個瀏覽器已通過網頁驗證)——套用中。",
|
|
129
143
|
needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// can no longer land.
|
|
6
6
|
// A panel with work that leaving would lose (Settings' staged changes, §5) sets a leave guard: every navigation of
|
|
7
7
|
// the app asks it first, Back/Forward included (the address bar is put back when it says no).
|
|
8
|
-
import { parseRoute, legacyHashTarget } from "./app-route.js";
|
|
8
|
+
import { parseRoute, legacyHashTarget, legacySettingsTarget } from "./app-route.js";
|
|
9
9
|
import { createStore } from "./app-store.js";
|
|
10
10
|
|
|
11
11
|
export const navStore = createStore({ route: null, seq: 0 });
|
|
@@ -28,7 +28,9 @@ const mayLeave = (route, path) => !guard || guard(route, path) !== false;
|
|
|
28
28
|
/** Start following the address bar. An old /ui#instance=<name> link becomes /ui/chat/<name> before the first render. */
|
|
29
29
|
export function startRouter(w = window) {
|
|
30
30
|
win = w;
|
|
31
|
-
|
|
31
|
+
let legacy = legacyHashTarget(w.location.pathname, w.location.hash) || legacySettingsTarget(w.location.pathname);
|
|
32
|
+
// An old Settings address with a place of its own (/settings/web#guests) keeps it when its new one names none.
|
|
33
|
+
if (legacy && w.location.hash && !legacy.includes("#") && legacySettingsTarget(w.location.pathname)) legacy += w.location.hash;
|
|
32
34
|
if (legacy) w.history.replaceState(null, "", legacy);
|
|
33
35
|
here = w.location.pathname + w.location.search;
|
|
34
36
|
navStore.set({ route: parseRoute(w.location.pathname), seq: 1 });
|
|
@@ -44,12 +46,15 @@ export function startRouter(w = window) {
|
|
|
44
46
|
|
|
45
47
|
/** Go to `path`. A path that is not a route of the app is a full load. `force` skips the leave guard (it said yes). */
|
|
46
48
|
export function navigate(path, opts = {}) {
|
|
47
|
-
|
|
49
|
+
let url = new URL(path, win.location.href);
|
|
50
|
+
// #2347: an old Settings address goes to its section's new one.
|
|
51
|
+
const moved = url.origin === win.location.origin ? legacySettingsTarget(url.pathname) : null;
|
|
52
|
+
if (moved) { const hash = url.hash; url = new URL(moved, win.location.href); if (!url.hash && hash) url.hash = hash; }
|
|
48
53
|
const route = parseRoute(url.pathname);
|
|
49
54
|
if (!route || url.origin !== win.location.origin) { win.location.assign(url.href); return; }
|
|
50
55
|
if (!opts.force && !mayLeave(route, url.pathname + url.search)) return;
|
|
51
|
-
if (url.pathname + url.search !== win.location.pathname + win.location.search) {
|
|
52
|
-
win.history[opts.replace ? "replaceState" : "pushState"](null, "", url.pathname + url.search);
|
|
56
|
+
if (url.pathname + url.search !== win.location.pathname + win.location.search || (url.hash && url.hash !== win.location.hash)) {
|
|
57
|
+
win.history[opts.replace ? "replaceState" : "pushState"](null, "", url.pathname + url.search + url.hash);
|
|
53
58
|
}
|
|
54
59
|
here = url.pathname + url.search;
|
|
55
60
|
navStore.set(s => ({ route, seq: s.seq + 1 }));
|
|
@@ -81,6 +81,16 @@ const inflight = new Map(); // key → writes sent and not answered yet
|
|
|
81
81
|
let flushTimer = null;
|
|
82
82
|
let writes = Promise.resolve(); // one write at a time, in the order they were made
|
|
83
83
|
const listeners = new Set();
|
|
84
|
+
// #2347: the "You" group's preferences (Settings → Preferences, Look) are saved by the Settings bar's Save like
|
|
85
|
+
// every Settings change. While Settings is open (holdPrefs), a choice there is shown on this page at once (cached, and
|
|
86
|
+
// this page's own in prefInForce) but not sent: `held` keeps it until savePrefsHeld sends them all, or discardHeldPrefs
|
|
87
|
+
// puts back what the host has (the same path as another device's change: apply and onPrefsChanged).
|
|
88
|
+
// Only keys Settings itself changes (a card's own size, card.sizeByInstance, is set from the card, outside Settings).
|
|
89
|
+
export const HELD_KEYS = Object.freeze(new Set(["theme", "lang", "names.primary", "preview.optIn", "fx.show", "color.show", "color.off", "card.size"]));
|
|
90
|
+
let holding = 0;
|
|
91
|
+
const held = new Map(); // key → value chosen in Settings, shown here, not saved yet
|
|
92
|
+
const heldFrom = new Map(); // key → its cached form before it was held (a choice back to it is no change)
|
|
93
|
+
const heldListeners = new Set();
|
|
84
94
|
// #2193: whether the signed-in person has a personal effect slot chosen (their fx.* keys: in the page's data-prefs and
|
|
85
95
|
// in every host read, never in this host cache). The effects runtime is loaded only when there is something to play.
|
|
86
96
|
const ownerEffectsIn = (values) => !!values && OWNER_EFFECT_PREF_KEYS.some((k) => own(values, k) && values[k] != null && values[k] !== "");
|
|
@@ -141,6 +151,7 @@ function take(body) {
|
|
|
141
151
|
*/
|
|
142
152
|
export function savePref(key, value) {
|
|
143
153
|
if (!hostKey(key)) return;
|
|
154
|
+
if (holding && HELD_KEYS.has(key)) { hold(key, value); return; }
|
|
144
155
|
cache(key, value);
|
|
145
156
|
if (!prefsWritable()) return;
|
|
146
157
|
pending.set(key, value);
|
|
@@ -149,6 +160,72 @@ export function savePref(key, value) {
|
|
|
149
160
|
flushTimer = setTimeout(flush, 400);
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
function hold(key, value) {
|
|
164
|
+
const k = SYNCED[key];
|
|
165
|
+
// What was in force before, as cached: never read back from this browser's copy, which the caller may have written
|
|
166
|
+
// already (theme.js stores the theme as it sets it).
|
|
167
|
+
if (!heldFrom.has(key)) { const f = prefInForce(key); heldFrom.set(key, f ? (f.value == null ? null : k.toLocal(f.value)) : readLocal(k.ls)); }
|
|
168
|
+
cache(key, value);
|
|
169
|
+
if ((value == null ? null : k.toLocal(value)) === heldFrom.get(key)) { // back to what it was: no change
|
|
170
|
+
held.delete(key); heldFrom.delete(key);
|
|
171
|
+
if (!pending.has(key) && !inflight.has(key)) mine.delete(key);
|
|
172
|
+
} else { held.set(key, value); mine.set(key, value); }
|
|
173
|
+
notifyHeld();
|
|
174
|
+
}
|
|
175
|
+
function notifyHeld() { for (const fn of [...heldListeners]) { try { fn(held.size); } catch { /* the rest still hear */ } } }
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* #2347: Settings is open — a "You" preference chosen now is held (shown, not saved) until savePrefsHeld or
|
|
179
|
+
* discardHeldPrefs. Returns the release; held choices left when the last hold goes (Settings closed without either) are
|
|
180
|
+
* put back.
|
|
181
|
+
*/
|
|
182
|
+
export function holdPrefs() {
|
|
183
|
+
holding++;
|
|
184
|
+
let released = false;
|
|
185
|
+
return () => {
|
|
186
|
+
if (released) return;
|
|
187
|
+
released = true;
|
|
188
|
+
holding--;
|
|
189
|
+
// A panel shown again at once (a language switch remounts it) holds again before this runs.
|
|
190
|
+
setTimeout(() => { if (!holding && held.size) discardHeldPrefs(); }, 0);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** How many "You" preferences are chosen and not saved yet. */
|
|
194
|
+
export function heldPrefsCount() { return held.size; }
|
|
195
|
+
/** Called with that count whenever it changes. Returns the unsubscribe. */
|
|
196
|
+
export function onHeldPrefs(fn) { heldListeners.add(fn); return () => heldListeners.delete(fn); }
|
|
197
|
+
/**
|
|
198
|
+
* Save every held choice (one write). Resolves when it has been answered; rejects when the host did not take it (what
|
|
199
|
+
* the host has is shown again). Not writable (the public link): this browser only, as before.
|
|
200
|
+
*/
|
|
201
|
+
export async function savePrefsHeld() {
|
|
202
|
+
const keys = [...held.keys()];
|
|
203
|
+
for (const key of keys) { if (prefsWritable()) pending.set(key, held.get(key)); else mine.delete(key); }
|
|
204
|
+
held.clear(); heldFrom.clear();
|
|
205
|
+
notifyHeld();
|
|
206
|
+
if (!keys.length || !prefsWritable()) return;
|
|
207
|
+
clearTimeout(flushTimer);
|
|
208
|
+
if ((await flush()) === false) throw new Error("preferences not saved");
|
|
209
|
+
}
|
|
210
|
+
/** Put back what the host has for every held choice, on this page (theme, language, … and every onPrefsChanged reader). */
|
|
211
|
+
export function discardHeldPrefs() {
|
|
212
|
+
const keys = [...held.keys()];
|
|
213
|
+
held.clear(); heldFrom.clear();
|
|
214
|
+
for (const key of keys) if (!pending.has(key) && !inflight.has(key)) mine.delete(key);
|
|
215
|
+
notifyHeld();
|
|
216
|
+
if (keys.length) align(keys, { force: true });
|
|
217
|
+
}
|
|
218
|
+
/** Run `fn` with nothing held (a confirmed action, such as Restore defaults, saves at once); its keys are held no more. */
|
|
219
|
+
export function withoutHold(fn) {
|
|
220
|
+
const h = holding;
|
|
221
|
+
holding = 0;
|
|
222
|
+
try { return fn(); } finally {
|
|
223
|
+
holding = h;
|
|
224
|
+
for (const key of [...held.keys()]) if (pending.has(key)) { held.delete(key); heldFrom.delete(key); }
|
|
225
|
+
notifyHeld();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
152
229
|
/** One write. ok: the host took it (its answer is a view; an older one than this page has is not taken). */
|
|
153
230
|
async function post(set, onlyIfUnset) {
|
|
154
231
|
const res = await fetch("/ui/prefs", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" },
|
|
@@ -173,7 +250,7 @@ function flush() {
|
|
|
173
250
|
for (const k of keys) {
|
|
174
251
|
const n = (inflight.get(k) || 1) - 1;
|
|
175
252
|
if (n) inflight.set(k, n); else inflight.delete(k);
|
|
176
|
-
if (!n && !pending.has(k)) { mine.delete(k); released.add(k); }
|
|
253
|
+
if (!n && !pending.has(k) && !held.has(k)) { mine.delete(k); released.add(k); }
|
|
177
254
|
}
|
|
178
255
|
// Each key whose last write this was, and not changed here again since, now shows the newest host view this page
|
|
179
256
|
// has — usually its own value; another device's newer one if that arrived while the write was out (held back then,
|
|
@@ -182,6 +259,7 @@ function flush() {
|
|
|
182
259
|
// the host (a storage that refused the choice) says nothing about the screen.
|
|
183
260
|
if (!ok) await refresh();
|
|
184
261
|
align(released, { force: true });
|
|
262
|
+
return ok;
|
|
185
263
|
});
|
|
186
264
|
return writes;
|
|
187
265
|
}
|
|
@@ -214,7 +292,7 @@ function align(keys, opts = {}) {
|
|
|
214
292
|
const changed = new Set();
|
|
215
293
|
for (const key of keys) {
|
|
216
294
|
if (!hostKey(key)) continue;
|
|
217
|
-
if (pending.has(key) || inflight.has(key)) continue;
|
|
295
|
+
if (pending.has(key) || inflight.has(key) || held.has(key)) continue;
|
|
218
296
|
const k = SYNCED[key];
|
|
219
297
|
const want = own(host, key) ? k.toLocal(host[key]) : null;
|
|
220
298
|
const copyDiffers = readLocal(k.ls) !== want;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
// #1408 §3: the app's routes on the client. The same table as the server's classifier (src/web-shell-routes.ts;
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export const
|
|
4
|
+
// #2347 §10: Config is gone from Fleet — each of its settings has one home in Settings (/ui/fleet/config leads there).
|
|
5
|
+
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "usage"];
|
|
6
|
+
// #2347: two groups — "You" changes only you or this browser; "Fleet" changes AgEnD for everyone.
|
|
7
|
+
export const SETTINGS_GROUPS = Object.freeze([
|
|
8
|
+
Object.freeze({ id: "you", sections: Object.freeze(["preferences", "look"]) }),
|
|
9
|
+
Object.freeze({ id: "fleet", sections: Object.freeze(["agents", "agent-defaults", "chat", "web-dashboard", "access", "advanced"]) }),
|
|
10
|
+
]);
|
|
11
|
+
export const SETTINGS_SECTIONS = Object.freeze(SETTINGS_GROUPS.flatMap((g) => g.sections));
|
|
12
|
+
/** #2347: the sections' names before the regrouping (links and bookmarks keep working) and where each one went. */
|
|
13
|
+
export const LEGACY_SETTINGS = Object.freeze({ device: "preferences", appearance: "look", general: "agent-defaults", bots: "chat", classic: "chat#classicbot", web: "access" });
|
|
14
|
+
const legacySection = (name) => (Object.prototype.hasOwnProperty.call(LEGACY_SETTINGS, name) ? LEGACY_SETTINGS[name] : null);
|
|
6
15
|
const MAX_INSTANCE_NAME = 128;
|
|
7
16
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
8
17
|
|
|
@@ -25,7 +34,7 @@ export function parseRoute(pathname) {
|
|
|
25
34
|
let m = /^\/ui\/fleet\/agent\/([^/]+)$/.exec(pathname);
|
|
26
35
|
if (m) return named("details", m[1]);
|
|
27
36
|
m = /^\/ui\/fleet\/([^/]+)$/.exec(pathname);
|
|
28
|
-
if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : null;
|
|
37
|
+
if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : m[1] === "config" ? { panel: "settings", section: "agent-defaults" } : null;
|
|
29
38
|
m = /^\/ui\/chat\/([^/]+)$/.exec(pathname);
|
|
30
39
|
if (m) return named("chat", m[1]);
|
|
31
40
|
// #1218: one instance's Log.
|
|
@@ -36,7 +45,11 @@ export function parseRoute(pathname) {
|
|
|
36
45
|
if (m) return named("view", m[1]);
|
|
37
46
|
if (pathname === "/settings") return { panel: "settings", section: "agents" };
|
|
38
47
|
m = /^\/settings\/([^/]+)$/.exec(pathname);
|
|
39
|
-
if (m)
|
|
48
|
+
if (m) {
|
|
49
|
+
if (SETTINGS_SECTIONS.includes(m[1])) return { panel: "settings", section: m[1] };
|
|
50
|
+
const moved = legacySection(m[1]);
|
|
51
|
+
return moved ? { panel: "settings", section: moved.split("#")[0] } : null;
|
|
52
|
+
}
|
|
40
53
|
return null;
|
|
41
54
|
}
|
|
42
55
|
function named(panel, segment) {
|
|
@@ -69,7 +82,28 @@ export function rememberFleetTab(tab) {
|
|
|
69
82
|
try { localStorage.setItem(LAST_FLEET_TAB_KEY, tab); } catch { /* this page only */ }
|
|
70
83
|
}
|
|
71
84
|
export const NEEDS_PATH = "/ui/needs";
|
|
72
|
-
|
|
85
|
+
/** A section's address; an old name gives its new one (and the place in it: `chat#classicbot`). */
|
|
86
|
+
export function settingsPath(name) {
|
|
87
|
+
const section = (name && legacySection(name)) || name;
|
|
88
|
+
return section && section !== "agents" ? `/settings/${section}` : "/settings";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* #2347 §7: a section's own address, always with its name (`/settings/agents` too). On a phone `/settings` is the list of
|
|
92
|
+
* sections, so going *to a section* (the nav, search, a field to fix, "Edit in Settings") uses this; `settingsPath()` is
|
|
93
|
+
* Settings itself (the app's menu and tabs).
|
|
94
|
+
*/
|
|
95
|
+
export function sectionPath(name) {
|
|
96
|
+
const path = settingsPath(name);
|
|
97
|
+
return path === "/settings" ? "/settings/agents" : path;
|
|
98
|
+
}
|
|
99
|
+
/** #2347: an old /settings/<name> address as its new one (with the place in it), or null. */
|
|
100
|
+
export function legacySettingsTarget(pathname) {
|
|
101
|
+
// #2347 §10: Fleet → Config's settings live in Settings now; its project folders are in Agent defaults.
|
|
102
|
+
if (pathname === "/ui/fleet/config") return "/settings/agent-defaults#project-roots";
|
|
103
|
+
const m = /^\/settings\/([^/]+)$/.exec(pathname || "");
|
|
104
|
+
const moved = m && legacySection(m[1]);
|
|
105
|
+
return moved ? settingsPath(moved) : null;
|
|
106
|
+
}
|
|
73
107
|
export function routePath(route) {
|
|
74
108
|
if (!route) return "/ui";
|
|
75
109
|
if (route.panel === "fleet") return fleetPath(route.tab);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { Icon } from "./ui-icons.js";
|
|
6
6
|
import { Toasts } from "./ui-toast.js";
|
|
7
|
-
import { ConfirmHost } from "./ui-confirm.js";
|
|
7
|
+
import { ConfirmHost, confirmDialog } from "./ui-confirm.js";
|
|
8
8
|
import { ColorDialogHost } from "./identity-color.js";
|
|
9
9
|
import { t, lang, setLang, onLang } from "./app-i18n.js";
|
|
10
10
|
import { appStore, createStore, guestBadge, guestPage, useStore } from "./app-store.js";
|
|
@@ -415,13 +415,13 @@ export function DevicePrefs({ labels = false } = {}) {
|
|
|
415
415
|
useEffect(() => onLang(setL), []);
|
|
416
416
|
return html`<div class=${`prefs${labels ? " labelled" : ""}`}>
|
|
417
417
|
<label class="pref"><${Icon} name="sun" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.theme")}</span>
|
|
418
|
-
<select value=${theme} aria-label=${t("app.theme")} onChange=${(e) => { setTheme(e.target.value); if (typeof AgendTheme !== "undefined") AgendTheme.set(e.target.value); savePref("theme", e.target.value); }}>
|
|
418
|
+
<select id=${labels ? "pref-theme" : undefined} value=${theme} aria-label=${t("app.theme")} onChange=${(e) => { setTheme(e.target.value); if (typeof AgendTheme !== "undefined") AgendTheme.set(e.target.value); savePref("theme", e.target.value); }}>
|
|
419
419
|
<option value="system">${t("app.themeSystem")}</option><option value="light">${t("app.themeLight")}</option><option value="dark">${t("app.themeDark")}</option></select></label>
|
|
420
420
|
<label class="pref"><${Icon} name="globe" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.language")}</span>
|
|
421
|
-
<select value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); savePref("lang", e.target.value); }}>
|
|
421
|
+
<select id=${labels ? "pref-lang" : undefined} value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); savePref("lang", e.target.value); }}>
|
|
422
422
|
<option value="en">English</option><option value="zh-TW">中文</option></select></label>
|
|
423
423
|
<label class="pref"><${Icon} name="user" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.names")}</span>
|
|
424
|
-
<select value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
|
|
424
|
+
<select id=${labels ? "pref-names" : undefined} value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
|
|
425
425
|
<option value="nickname">${t("app.namesNickname")}</option><option value="instance">${t("app.namesInstance")}</option></select></label>
|
|
426
426
|
</div>`;
|
|
427
427
|
}
|
|
@@ -586,6 +586,60 @@ function OperationLine({ route }) {
|
|
|
586
586
|
<span>${text}</span> <a class="btn btn-sm" href=${settingsPath()}>${t("app.openSettings")}</a></div>`;
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
/**
|
|
590
|
+
* #2347: saved changes that wait for an AgEnD restart, as the server says (GET /api/settings/restart-pending), into
|
|
591
|
+
* appStore.restartPending ({ keys, jobId } or null). A failed read keeps what was known.
|
|
592
|
+
*/
|
|
593
|
+
export async function refreshRestartPending() {
|
|
594
|
+
let body;
|
|
595
|
+
try {
|
|
596
|
+
const r = await fetch("/api/settings/restart-pending");
|
|
597
|
+
if (!r.ok) return;
|
|
598
|
+
body = await r.json();
|
|
599
|
+
} catch { return; }
|
|
600
|
+
const keys = body && Array.isArray(body.keys) ? body.keys.filter((k) => typeof k === "string") : [];
|
|
601
|
+
appStore.set({ restartPending: body && body.pending === true
|
|
602
|
+
? { keys, jobId: typeof body.job_id === "string" && body.job_id ? body.job_id : null } : null });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* #2347: the app-wide line for saved changes that wait for an AgEnD restart — in every panel, until AgEnD restarts.
|
|
607
|
+
* Restart now asks first and is never automatic (the leader's default: a Save that restarted on its own once took 33
|
|
608
|
+
* agents down at once). Asked again whenever a Save or a restart settles. Left to the Save in hand while it runs or offers
|
|
609
|
+
* its own restart (Settings' card, or the line above in another panel). Over a temporary public link it says so and
|
|
610
|
+
* offers nothing; with no Settings job to carry the restart (a hand edit), it says how to restart on the host.
|
|
611
|
+
*/
|
|
612
|
+
function RestartLine() {
|
|
613
|
+
const { restartPending: pending, settingsOp: op, restartRunner } = useStore(appStore);
|
|
614
|
+
const [later, setLater] = useState(null);
|
|
615
|
+
const [busy, setBusy] = useState(false);
|
|
616
|
+
const settled = !op || (!["writing", "posting", "watching"].includes(op.phase) && op.restart !== "busy");
|
|
617
|
+
useEffect(() => { if (settled) refreshRestartPending(); }, [op ? `${op.id}:${op.phase}:${op.restart}` : "none"]);
|
|
618
|
+
if (!pending || !settled) return null;
|
|
619
|
+
if (op && op.job && op.job.targets && op.job.targets.some((r) => r.target === "fleet" && r.status === "restart-required")) return null;
|
|
620
|
+
const seen = `${pending.jobId}|${pending.keys.join(",")}`;
|
|
621
|
+
if (later === seen) return null;
|
|
622
|
+
const canRestart = !!pending.jobId && typeof restartRunner === "function";
|
|
623
|
+
const restart = async () => {
|
|
624
|
+
if (!(await confirmDialog({ title: t("app.restartNow"), message: t("app.restartConfirm"), confirmLabel: t("app.restartNow"), danger: true }))) return;
|
|
625
|
+
setBusy(true);
|
|
626
|
+
let res;
|
|
627
|
+
try { res = await restartRunner(pending.jobId); } catch { res = { ok: false }; }
|
|
628
|
+
setBusy(false);
|
|
629
|
+
if (res && !res.ok && !res.stale && !res.busy) {
|
|
630
|
+
toast(t("app.restartRefused", (res.body && res.body.error) || t("app.failed")), false);
|
|
631
|
+
// #2378 review: a refusal may mean it is done already (restarted from another tab or the host): ask again.
|
|
632
|
+
refreshRestartPending();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
return html`<div data-trust="status" class="conn restart-line lbar" role="status">
|
|
636
|
+
<span>${t("app.restartWaiting", Math.max(1, pending.keys.length))}</span>
|
|
637
|
+
${pending.keys.length ? html`<span class="muted restart-keys">${pending.keys.join(", ")}</span>` : null}
|
|
638
|
+
${!pending.jobId ? html`<span class="muted">${t("app.restartOnHost")}</span>` : null}
|
|
639
|
+
${canRestart ? html`<button type="button" class="btn btn-sm btn-primary" disabled=${busy} onClick=${restart}>${t("app.restartNow")}</button>` : null}
|
|
640
|
+
<button type="button" class="btn btn-sm" onClick=${() => setLater(seen)}>${t("app.restartLater")}</button></div>`;
|
|
641
|
+
}
|
|
642
|
+
|
|
589
643
|
/**
|
|
590
644
|
* A module loader that can be tried again: a failed attempt is forgotten (the next call loads afresh) and `attempt`
|
|
591
645
|
* counts, so a retry can ask for a new URL — a browser may keep a failed module import cached under the old one.
|
|
@@ -729,6 +783,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
729
783
|
<${ConnectionLine} />
|
|
730
784
|
<${StartupLine} />
|
|
731
785
|
${viewOnly || guest ? null : html`<${OperationLine} route=${nav.route} />`}
|
|
786
|
+
${viewOnly || guest ? null : html`<${RestartLine} />`}
|
|
732
787
|
${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
|
|
733
788
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
734
789
|
</main>
|
|
@@ -35,7 +35,8 @@
|
|
|
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
37
|
// offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
|
|
38
|
-
|
|
38
|
+
// office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
|
|
39
|
+
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", "offices", "office_skins"];
|
|
39
40
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
40
41
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
41
42
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|