@songsid/agend 2.3.1 → 2.3.2-beta.10
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/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +5 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/antigravity-usage-scan.d.ts +58 -0
- package/dist/antigravity-usage-scan.js +257 -0
- package/dist/antigravity-usage-scan.js.map +1 -0
- package/dist/antigravity-usage-worker.d.ts +1 -0
- package/dist/antigravity-usage-worker.js +10 -0
- package/dist/antigravity-usage-worker.js.map +1 -0
- package/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- 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/backend/kiro-identity.d.ts +64 -0
- package/dist/backend/kiro-identity.js +101 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-retired-model.d.ts +62 -0
- package/dist/backend/kiro-retired-model.js +160 -0
- package/dist/backend/kiro-retired-model.js.map +1 -0
- package/dist/backend/kiro.d.ts +71 -2
- package/dist/backend/kiro.js +352 -11
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +59 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-signin-guide.d.ts +1 -0
- package/dist/backend-signin-guide.js +9 -0
- package/dist/backend-signin-guide.js.map +1 -0
- package/dist/backend-signin-pending.d.ts +10 -0
- package/dist/backend-signin-pending.js +44 -0
- package/dist/backend-signin-pending.js.map +1 -0
- package/dist/browser-backend-signins.d.ts +56 -0
- package/dist/browser-backend-signins.js +189 -0
- package/dist/browser-backend-signins.js.map +1 -0
- package/dist/cache-ledger.d.ts +38 -2
- package/dist/cache-ledger.js +133 -8
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-prices.d.ts +2 -1
- package/dist/cache-prices.js +5 -2
- package/dist/cache-prices.js.map +1 -1
- package/dist/cache-service.d.ts +68 -0
- package/dist/cache-service.js +355 -13
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +13 -0
- package/dist/channel/adapters/discord.js +103 -16
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +34 -7
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/classic-channel-manager.d.ts +11 -0
- package/dist/classic-channel-manager.js +39 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +21 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +92 -12
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +31 -1
- package/dist/config.js.map +1 -1
- package/dist/cpu-quota.d.ts +23 -0
- package/dist/cpu-quota.js +86 -0
- package/dist/cpu-quota.js.map +1 -0
- package/dist/daemon.d.ts +57 -3
- package/dist/daemon.js +330 -50
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +20 -6
- package/dist/dashboard-permissions.js +58 -9
- package/dist/dashboard-permissions.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 +11 -1
- 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 +212 -0
- package/dist/fleet-manager.js +914 -111
- 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/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
- 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 +14 -6
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +20 -0
- package/dist/instance-lifecycle.js +71 -13
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +5 -0
- package/dist/kiro-db-reader.js +18 -0
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +9 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-migrate.d.ts +49 -0
- package/dist/kiro-migrate.js +105 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +2 -0
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/kiro-transcript-worker.js +1 -0
- package/dist/kiro-transcript-worker.js.map +1 -1
- package/dist/kiro-tui-forensic.d.ts +7 -0
- package/dist/kiro-tui-forensic.js +110 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/kiro-usage-run.d.ts +7 -0
- package/dist/kiro-usage-run.js +39 -0
- package/dist/kiro-usage-run.js.map +1 -0
- package/dist/kiro-usage-scan.d.ts +49 -0
- package/dist/kiro-usage-scan.js +223 -0
- package/dist/kiro-usage-scan.js.map +1 -0
- package/dist/kiro-usage-worker.d.ts +1 -0
- package/dist/kiro-usage-worker.js +10 -0
- package/dist/kiro-usage-worker.js.map +1 -0
- package/dist/kiro-usage.d.ts +58 -0
- package/dist/kiro-usage.js +310 -0
- package/dist/kiro-usage.js.map +1 -0
- package/dist/locale.js +120 -12
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/login-url.d.ts +4 -0
- package/dist/login-url.js +22 -0
- package/dist/login-url.js.map +1 -0
- 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/needs-you-hub.d.ts +2 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +8 -2
- package/dist/needs-you.js +6 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/notice-delivery.d.ts +16 -0
- package/dist/notice-delivery.js +35 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/office-share-page.d.ts +16 -0
- package/dist/office-share-page.js +85 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +91 -0
- package/dist/office-share-service.js +176 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +258 -0
- package/dist/office-share.js +549 -0
- package/dist/office-share.js.map +1 -0
- package/dist/opencode-usage-scan.d.ts +28 -0
- package/dist/opencode-usage-scan.js +92 -0
- package/dist/opencode-usage-scan.js.map +1 -0
- package/dist/opencode-usage-worker.d.ts +1 -0
- package/dist/opencode-usage-worker.js +10 -0
- package/dist/opencode-usage-worker.js.map +1 -0
- package/dist/outbound-handlers.js +22 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +28 -2
- package/dist/outbound-schemas.js +29 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +16 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.js +13 -2
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.js +2 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/reply-triggers.d.ts +2 -0
- package/dist/reply-triggers.js +3 -0
- package/dist/reply-triggers.js.map +1 -0
- package/dist/settings-api.d.ts +28 -0
- package/dist/settings-api.js +148 -17
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +54 -17
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +12 -3
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +4 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +55 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +127 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-signin-assets.d.ts +2 -0
- package/dist/setup-signin-assets.js +6 -0
- package/dist/setup-signin-assets.js.map +1 -0
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.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/structured-signin-transport.d.ts +8 -0
- package/dist/structured-signin-transport.js +79 -0
- package/dist/structured-signin-transport.js.map +1 -0
- package/dist/structured-signin.d.ts +131 -0
- package/dist/structured-signin.js +388 -0
- package/dist/structured-signin.js.map +1 -0
- package/dist/tool-permissions.js +12 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +6 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +26 -2
- package/dist/transcript-sources.js +103 -40
- package/dist/transcript-sources.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 +46 -9
- package/dist/ui/backend-signin.js +68 -12
- 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 +4 -4
- 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 +94 -14
- package/dist/ui/fleet-office-layout.js +235 -0
- package/dist/ui/fleet-office-model.js +12 -6
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +44 -0
- package/dist/ui/fleet-office-pack.js +352 -37
- package/dist/ui/fleet-office-skin.js +220 -36
- package/dist/ui/fleet-office-store.js +112 -7
- package/dist/ui/fleet-office-view.js +155 -0
- package/dist/ui/fleet-office-zones.js +264 -42
- package/dist/ui/fleet-office.js +957 -160
- package/dist/ui/fleet-usage.js +26 -21
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +164 -0
- package/dist/ui/panel-chat.js +9 -2
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +4 -4
- package/dist/ui/panel-settings.js +608 -167
- 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 +76 -23
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +255 -79
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +16 -6
- package/dist/ui/settings-registry.js +146 -0
- package/dist/ui/settings-strings.js +116 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +18 -2
- 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 +73 -10
- package/dist/ui/shared/app-stream.js +3 -1
- package/dist/ui/shared/app.css +307 -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/office/cozy.png +0 -0
- package/dist/ui/shared/side-panel.js +95 -0
- 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/ui/structured-signin.js +115 -0
- package/dist/usage-report.d.ts +20 -0
- package/dist/usage-report.js +54 -17
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +9 -1
- package/dist/usage-rollup.js +20 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +26 -2
- package/dist/web-api.js +361 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +8 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.d.ts +1 -7
- package/dist/web-backend-login.js +2 -24
- package/dist/web-backend-login.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-acts.d.ts +173 -0
- package/dist/web-office-acts.js +285 -0
- package/dist/web-office-acts.js.map +1 -0
- package/dist/web-office-skins.d.ts +109 -0
- package/dist/web-office-skins.js +299 -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 +77 -14
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +8 -1
- package/dist/web-settings-patch.js +65 -5
- 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 +298 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +5 -1
- package/dist/working-state-journal.js +31 -3
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +155 -140
|
@@ -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,14 +93,19 @@ 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",
|
|
93
103
|
// #2293: global search — chats, cross-instance messages and tasks (not the sidebar's filter of agents).
|
|
94
104
|
search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
|
|
95
105
|
confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
|
|
106
|
+
sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
|
|
96
107
|
// The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
|
|
97
|
-
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
|
|
108
|
+
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
|
|
98
109
|
},
|
|
99
110
|
"zh-TW": {
|
|
100
111
|
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
|
|
@@ -124,11 +135,16 @@ register("app", {
|
|
|
124
135
|
opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
|
|
125
136
|
opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
|
|
126
137
|
opFailed: "設定沒有套用", openSettings: "開啟設定",
|
|
138
|
+
restartWaiting: "{0} 項已儲存的變更要在 AgEnD 重新啟動後才會生效", restartNow: "立即重新啟動", restartLater: "稍後",
|
|
139
|
+
restartConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
140
|
+
restartOnHost: "請在主機上執行 agend restart 讓它們生效。",
|
|
141
|
+
restartRefused: "重新啟動被拒絕:{0}",
|
|
127
142
|
pendingSent: "已送出確認——由 fleet 管理員在聊天中確認。",
|
|
128
143
|
pendingSelfSent: "已由你的帳號確認(這個瀏覽器已通過網頁驗證)——套用中。",
|
|
129
144
|
needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
|
|
130
145
|
search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
|
|
131
146
|
confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
|
|
132
|
-
|
|
147
|
+
sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
|
|
148
|
+
needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
|
|
133
149
|
},
|
|
134
150
|
});
|
|
@@ -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";
|
|
@@ -16,7 +16,8 @@ import { onPrefsChanged, savePref } from "./app-prefs.js";
|
|
|
16
16
|
import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
|
|
17
17
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
18
18
|
import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
|
|
19
|
-
import {
|
|
19
|
+
import { usageRailStore } from "./header-tools.js";
|
|
20
|
+
import { RightRail, sidePanelStore } from "./side-panel.js";
|
|
20
21
|
import { OwnerCssKeepBar } from "./owner-css.js";
|
|
21
22
|
import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
|
|
22
23
|
|
|
@@ -89,14 +90,17 @@ export function openSearch(preset = {}) {
|
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
92
|
* Ctrl+K (⌘K) anywhere opens search; Ctrl+F (⌘F) in an agent's chat opens it narrowed to that agent — elsewhere the
|
|
92
|
-
* browser's own find stays. Not while another dialog is open (search, open already, has its input focused)
|
|
93
|
+
* browser's own find stays. Not while another dialog is open (search, open already, has its input focused); a docked
|
|
94
|
+
* side panel (#2585) is not one.
|
|
93
95
|
*/
|
|
94
96
|
export function handleSearchKey(e) {
|
|
95
97
|
if (!searchOffered(true)) return;
|
|
96
98
|
if (e.isComposing || e.altKey || e.shiftKey || !(isMacLike() ? e.metaKey : e.ctrlKey)) return;
|
|
97
99
|
const key = (e.key || "").toLowerCase();
|
|
98
100
|
if (key !== "k" && key !== "f") return;
|
|
99
|
-
|
|
101
|
+
// #2585: a docked side panel is not modal — the page beside it is in use, so search is too (a result that leaves
|
|
102
|
+
// the page asks first when the panel holds changes: side-panel.js).
|
|
103
|
+
if (typeof document !== "undefined" && document.querySelector("dialog[open]:not(.side-panel)")) return;
|
|
100
104
|
const route = navStore.get().route;
|
|
101
105
|
if (key === "f" && !(route && route.panel === "chat" && route.instance)) return;
|
|
102
106
|
e.preventDefault();
|
|
@@ -127,7 +131,7 @@ export const sidebarWidthStore = createStore({ width: null });
|
|
|
127
131
|
function mainBudget() {
|
|
128
132
|
const vw = viewportWidth();
|
|
129
133
|
if (vw < 1200 || typeof document === "undefined") return vw;
|
|
130
|
-
const rail = document.querySelector(".u-rail");
|
|
134
|
+
const rail = document.querySelector(".shell > .u-rail, .shell > .side-panel"); // #2585: a docked panel in its place
|
|
131
135
|
if (!rail) return vw;
|
|
132
136
|
const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
|
|
133
137
|
return vw - w;
|
|
@@ -181,11 +185,15 @@ function SidebarResize() {
|
|
|
181
185
|
// once the page has drawn its change.
|
|
182
186
|
const again = () => afterPaint(applyKeptSidebarWidth);
|
|
183
187
|
const offRail = usageRailStore.subscribe(again);
|
|
188
|
+
// #2585: a docked panel in the rail's place (it is published on every render of its editor: only its coming and
|
|
189
|
+
// going changes the room).
|
|
190
|
+
let docked = !!sidePanelStore.get().panel;
|
|
191
|
+
const offPanel = sidePanelStore.subscribe(() => { const now = !!sidePanelStore.get().panel; if (now !== docked) { docked = now; again(); } });
|
|
184
192
|
const offNav = navStore.subscribe(again);
|
|
185
193
|
const w = win();
|
|
186
194
|
if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
|
|
187
195
|
return () => {
|
|
188
|
-
offRail(); offNav();
|
|
196
|
+
offRail(); offPanel(); offNav();
|
|
189
197
|
if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
|
|
190
198
|
if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
|
|
191
199
|
};
|
|
@@ -415,13 +423,13 @@ export function DevicePrefs({ labels = false } = {}) {
|
|
|
415
423
|
useEffect(() => onLang(setL), []);
|
|
416
424
|
return html`<div class=${`prefs${labels ? " labelled" : ""}`}>
|
|
417
425
|
<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); }}>
|
|
426
|
+
<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
427
|
<option value="system">${t("app.themeSystem")}</option><option value="light">${t("app.themeLight")}</option><option value="dark">${t("app.themeDark")}</option></select></label>
|
|
420
428
|
<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); }}>
|
|
429
|
+
<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
430
|
<option value="en">English</option><option value="zh-TW">中文</option></select></label>
|
|
423
431
|
<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)}>
|
|
432
|
+
<select id=${labels ? "pref-names" : undefined} value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
|
|
425
433
|
<option value="nickname">${t("app.namesNickname")}</option><option value="instance">${t("app.namesInstance")}</option></select></label>
|
|
426
434
|
</div>`;
|
|
427
435
|
}
|
|
@@ -586,6 +594,60 @@ function OperationLine({ route }) {
|
|
|
586
594
|
<span>${text}</span> <a class="btn btn-sm" href=${settingsPath()}>${t("app.openSettings")}</a></div>`;
|
|
587
595
|
}
|
|
588
596
|
|
|
597
|
+
/**
|
|
598
|
+
* #2347: saved changes that wait for an AgEnD restart, as the server says (GET /api/settings/restart-pending), into
|
|
599
|
+
* appStore.restartPending ({ keys, jobId } or null). A failed read keeps what was known.
|
|
600
|
+
*/
|
|
601
|
+
export async function refreshRestartPending() {
|
|
602
|
+
let body;
|
|
603
|
+
try {
|
|
604
|
+
const r = await fetch("/api/settings/restart-pending");
|
|
605
|
+
if (!r.ok) return;
|
|
606
|
+
body = await r.json();
|
|
607
|
+
} catch { return; }
|
|
608
|
+
const keys = body && Array.isArray(body.keys) ? body.keys.filter((k) => typeof k === "string") : [];
|
|
609
|
+
appStore.set({ restartPending: body && body.pending === true
|
|
610
|
+
? { keys, jobId: typeof body.job_id === "string" && body.job_id ? body.job_id : null } : null });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* #2347: the app-wide line for saved changes that wait for an AgEnD restart — in every panel, until AgEnD restarts.
|
|
615
|
+
* Restart now asks first and is never automatic (the leader's default: a Save that restarted on its own once took 33
|
|
616
|
+
* agents down at once). Asked again whenever a Save or a restart settles. Left to the Save in hand while it runs or offers
|
|
617
|
+
* its own restart (Settings' card, or the line above in another panel). Over a temporary public link it says so and
|
|
618
|
+
* offers nothing; with no Settings job to carry the restart (a hand edit), it says how to restart on the host.
|
|
619
|
+
*/
|
|
620
|
+
function RestartLine() {
|
|
621
|
+
const { restartPending: pending, settingsOp: op, restartRunner } = useStore(appStore);
|
|
622
|
+
const [later, setLater] = useState(null);
|
|
623
|
+
const [busy, setBusy] = useState(false);
|
|
624
|
+
const settled = !op || (!["writing", "posting", "watching"].includes(op.phase) && op.restart !== "busy");
|
|
625
|
+
useEffect(() => { if (settled) refreshRestartPending(); }, [op ? `${op.id}:${op.phase}:${op.restart}` : "none"]);
|
|
626
|
+
if (!pending || !settled) return null;
|
|
627
|
+
if (op && op.job && op.job.targets && op.job.targets.some((r) => r.target === "fleet" && r.status === "restart-required")) return null;
|
|
628
|
+
const seen = `${pending.jobId}|${pending.keys.join(",")}`;
|
|
629
|
+
if (later === seen) return null;
|
|
630
|
+
const canRestart = !!pending.jobId && typeof restartRunner === "function";
|
|
631
|
+
const restart = async () => {
|
|
632
|
+
if (!(await confirmDialog({ title: t("app.restartNow"), message: t("app.restartConfirm"), confirmLabel: t("app.restartNow"), danger: true }))) return;
|
|
633
|
+
setBusy(true);
|
|
634
|
+
let res;
|
|
635
|
+
try { res = await restartRunner(pending.jobId); } catch { res = { ok: false }; }
|
|
636
|
+
setBusy(false);
|
|
637
|
+
if (res && !res.ok && !res.stale && !res.busy) {
|
|
638
|
+
toast(t("app.restartRefused", (res.body && res.body.error) || t("app.failed")), false);
|
|
639
|
+
// #2378 review: a refusal may mean it is done already (restarted from another tab or the host): ask again.
|
|
640
|
+
refreshRestartPending();
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
return html`<div data-trust="status" class="conn restart-line lbar" role="status">
|
|
644
|
+
<span>${t("app.restartWaiting", Math.max(1, pending.keys.length))}</span>
|
|
645
|
+
${pending.keys.length ? html`<span class="muted restart-keys">${pending.keys.join(", ")}</span>` : null}
|
|
646
|
+
${!pending.jobId ? html`<span class="muted">${t("app.restartOnHost")}</span>` : null}
|
|
647
|
+
${canRestart ? html`<button type="button" class="btn btn-sm btn-primary" disabled=${busy} onClick=${restart}>${t("app.restartNow")}</button>` : null}
|
|
648
|
+
<button type="button" class="btn btn-sm" onClick=${() => setLater(seen)}>${t("app.restartLater")}</button></div>`;
|
|
649
|
+
}
|
|
650
|
+
|
|
589
651
|
/**
|
|
590
652
|
* A module loader that can be tried again: a failed attempt is forgotten (the next call loads afresh) and `attempt`
|
|
591
653
|
* counts, so a retry can ask for a new URL — a browser may keep a failed module import cached under the old one.
|
|
@@ -729,10 +791,11 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
729
791
|
<${ConnectionLine} />
|
|
730
792
|
<${StartupLine} />
|
|
731
793
|
${viewOnly || guest ? null : html`<${OperationLine} route=${nav.route} />`}
|
|
794
|
+
${viewOnly || guest ? null : html`<${RestartLine} />`}
|
|
732
795
|
${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
|
|
733
796
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
734
797
|
</main>
|
|
735
|
-
${guest ? null : html`<${
|
|
798
|
+
${guest ? null : html`<${RightRail} route=${nav.route} />`}
|
|
736
799
|
<${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
|
|
737
800
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
738
801
|
${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
@@ -35,7 +35,9 @@
|
|
|
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
|
+
// office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
|
|
40
|
+
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", "office_acts"];
|
|
39
41
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
40
42
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
41
43
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|