@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/agent-cli.js +4 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +1 -0
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +5 -0
- package/dist/agent-whats-new.js +16 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +7 -0
- package/dist/auth-api.js +8 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex-package.d.ts +30 -0
- package/dist/backend/codex-package.js +133 -0
- package/dist/backend/codex-package.js.map +1 -0
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +9 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -1
- package/dist/backend/kiro.js +102 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode-permission.js +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js +2 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/binary-probe.js +1 -1
- package/dist/bug-report-cli.js +1 -1
- package/dist/bundled-skills.d.ts +7 -3
- package/dist/bundled-skills.js +16 -7
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +12 -4
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +3 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +54 -0
- package/dist/child-process.js.map +1 -0
- package/dist/cli.js +167 -182
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +20 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +31 -8
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.js +1 -1
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +42 -0
- package/dist/doctor.js.map +1 -1
- package/dist/export-import.js +1 -1
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +45 -0
- package/dist/fleet-manager.js +301 -21
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.d.ts +5 -5
- package/dist/full-restart.js +5 -5
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -3
- package/dist/general-knowledge/steering/core-rules.md +1 -1
- package/dist/instance-config-impact.js +10 -3
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +4 -4
- package/dist/locale.js +118 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.js +1 -1
- package/dist/login-shell.js +3 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/ls-rows.js +1 -1
- package/dist/needs-you-hub.d.ts +98 -1
- package/dist/needs-you-hub.js +313 -44
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +5 -2
- package/dist/needs-you-live.js +6 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +58 -3
- package/dist/needs-you.js +36 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -3
- package/dist/public-link-progress.d.ts +12 -1
- package/dist/public-link-progress.js +5 -10
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +5 -0
- package/dist/public-tunnel-config.js +45 -1
- package/dist/public-tunnel-config.js.map +1 -1
- package/dist/public-web-link.js +6 -5
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart.js +14 -8
- package/dist/quickstart.js.map +1 -1
- package/dist/resource-report.js +1 -1
- package/dist/scheduler/task-board.d.ts +41 -1
- package/dist/scheduler/task-board.js +150 -4
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.js +2 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +69 -0
- package/dist/service-installer.js +181 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +44 -0
- package/dist/service-owner.js +48 -0
- package/dist/service-owner.js.map +1 -0
- package/dist/settings-api.d.ts +4 -0
- package/dist/settings-api.js +21 -3
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +85 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +2 -0
- package/dist/settings-http-confirmation.js +3 -2
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +4 -1
- package/dist/settings-request-capability.js +11 -3
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/setup-host.js +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-summary.d.ts +101 -0
- package/dist/task-board-summary.js +177 -0
- package/dist/task-board-summary.js.map +1 -0
- package/dist/task-board-waker.d.ts +8 -1
- package/dist/task-board-waker.js +15 -6
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.js +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +5 -1
- package/dist/tmux-exec-helper.js +13 -21
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.js +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -0
- package/dist/topic-commands.js +29 -18
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +5 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +2 -0
- package/dist/transcript-sources.js +4 -0
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js +3 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +1 -1
- package/dist/tunnel/cloudflared.js +51 -10
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +13 -0
- package/dist/tunnel/named-diagnostics.js +52 -0
- package/dist/tunnel/named-diagnostics.js.map +1 -0
- package/dist/tunnel/types.d.ts +2 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/ui/app-effects.js +10 -4
- package/dist/ui/chat-strings.js +3 -0
- package/dist/ui/chat-thread.js +14 -7
- package/dist/ui/effect-trial-offer.js +2 -31
- package/dist/ui/effects-waker.js +2 -70
- package/dist/ui/fleet-task-card.js +5 -5
- package/dist/ui/fleet-task-common.js +41 -7
- package/dist/ui/fleet-task-work.js +107 -0
- package/dist/ui/fleet-tasks.js +46 -12
- package/dist/ui/image-lightbox.js +39 -1
- package/dist/ui/image-zoom.js +242 -0
- package/dist/ui/panel-chat.js +24 -13
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +6 -2
- package/dist/ui/panel-needs.js +21 -6
- package/dist/ui/panel-settings.js +35 -13
- package/dist/ui/preview-panel.js +2 -1
- package/dist/ui/search-dialog.js +2 -1
- package/dist/ui/settings-appearance.js +33 -8
- package/dist/ui/settings-dialogs.js +33 -17
- package/dist/ui/settings-discord.js +6 -2
- package/dist/ui/settings-guests.js +2 -1
- package/dist/ui/settings-model.js +2 -11
- package/dist/ui/settings-mods.js +6 -3
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/settings-token.js +3 -1
- package/dist/ui/settings-web-authority.js +6 -3
- package/dist/ui/settings-wizard.js +13 -7
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +2 -2
- package/dist/ui/shared/app-store.js +2 -2
- package/dist/ui/shared/app.css +80 -9
- package/dist/ui/shared/app.js +5 -1
- package/dist/ui/shared/effect-app-runtime.js +7 -5
- package/dist/ui/shared/effect-chat-runtime.js +2 -7
- package/dist/ui/shared/effect-cursor.js +3 -2
- package/dist/ui/shared/effect-display-metadata.js +39 -0
- package/dist/ui/shared/effect-registry.js +4 -2
- package/dist/ui/shared/effect-runtime-loader.js +3 -1
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/feature-flags.js +127 -0
- package/dist/ui/shared/identity-color.js +6 -3
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/text-undo.js +210 -0
- package/dist/ui/shared/ui-dialog.js +4 -3
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/update-dispatch.js +1 -1
- package/dist/update-launch.d.ts +1 -1
- package/dist/update-launch.js +1 -2
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.js +1 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +152 -14
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +1 -0
- package/dist/web-appearance-tools.js +7 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance.d.ts +4 -10
- package/dist/web-appearance.js +4 -3
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-chat-history.d.ts +14 -0
- package/dist/web-chat-history.js +26 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-experimental-http.d.ts +2 -0
- package/dist/web-experimental-http.js +12 -0
- package/dist/web-experimental-http.js.map +1 -0
- package/dist/web-experimental.d.ts +11 -0
- package/dist/web-experimental.js +49 -0
- package/dist/web-experimental.js.map +1 -0
- package/dist/web-guest-access.d.ts +1 -0
- package/dist/web-guest-access.js +4 -2
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -0
- package/dist/web-mod-store.js +10 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-preview.d.ts +2 -0
- package/dist/web-preview.js +2 -0
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +5 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +1 -0
- package/dist/web-settings-tools.js +14 -10
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal.js +1 -1
- package/dist/web-theme-store.d.ts +2 -2
- package/dist/web-theme-store.js +6 -3
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/settings-mods.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// parts the person ticks are applied, all or nothing. Custom CSS from a file is never ticked by default. Installed
|
|
5
5
|
// mods can be downloaded again or uninstalled (what they applied stays: Reset and the CSS controls work on it).
|
|
6
6
|
// Export builds a file from what is in force now.
|
|
7
|
+
import { canvasWorkerEnabled, useCanvasWorker } from "/assets/feature-flags.js";
|
|
7
8
|
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
8
9
|
import { t } from "/assets/app-i18n.js";
|
|
9
10
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
@@ -45,7 +46,7 @@ export function defaultChoice(plan) {
|
|
|
45
46
|
for (const it of plan.items) {
|
|
46
47
|
if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
|
|
47
48
|
else if (it.part === "widget") c.widgets[it.slot] = true;
|
|
48
|
-
else if (it.part === "effect") c.effects = true;
|
|
49
|
+
else if (it.part === "effect" && canvasWorkerEnabled()) c.effects = true;
|
|
49
50
|
// ownerCss stays false: "read it before turning it on" (r4 §4).
|
|
50
51
|
}
|
|
51
52
|
return c;
|
|
@@ -80,7 +81,7 @@ export function ThemeDetail({ theme, pictures, effects }) {
|
|
|
80
81
|
${th.background ? pic(th.background.image, tn("modBackground")) : null}
|
|
81
82
|
${th.background ? row(tn("modBackgroundFit"), `${th.background.fit} · ${Math.round(th.background.opacity * 100)}%`) : null}
|
|
82
83
|
${th.avatar ? pic(th.avatar, tn("modAvatar")) : null}
|
|
83
|
-
${(effects || []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
|
|
84
|
+
${(canvasWorkerEnabled() ? effects || [] : []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
|
|
84
85
|
<details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify(th, null, 2)}</pre></details>
|
|
85
86
|
</div>`;
|
|
86
87
|
}
|
|
@@ -114,6 +115,7 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
|
|
|
114
115
|
<details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify({ loop: it.loop, click: it.click }, null, 2)}</pre></details></div></li>`;
|
|
115
116
|
}
|
|
116
117
|
if (it.part === "effect") {
|
|
118
|
+
if (!canvasWorkerEnabled()) return null;
|
|
117
119
|
return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
|
|
118
120
|
<div class="s-mod-detail"><span>${kib(it.bytes)} · ${tn("modRegions", it.regions.join(", "))}</span>${it.wants.length ? html`<span>${tn("modWants", it.wants.join(", "))}</span>` : null}</div></li>`;
|
|
119
121
|
}
|
|
@@ -137,7 +139,7 @@ function Plan({ staged, onDone }) {
|
|
|
137
139
|
const names = (app.instances || []).map((i) => i.name);
|
|
138
140
|
const install = async () => {
|
|
139
141
|
setBusy(true); setError("");
|
|
140
|
-
const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: choice, ...(toInstance ? { target } : {}) });
|
|
142
|
+
const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: canvasWorkerEnabled() && choice.effects }, ...(toInstance ? { target } : {}) });
|
|
141
143
|
setBusy(false);
|
|
142
144
|
// The stage this plan shows is gone or no longer this plan (replaced on another device, expired): read again —
|
|
143
145
|
// the page then shows whatever is staged now, with its own, fresh choices (keyed by stage).
|
|
@@ -205,6 +207,7 @@ function ExportForm() {
|
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
export function ModsCard() {
|
|
210
|
+
useCanvasWorker();
|
|
208
211
|
const [state, setState] = useState(null);
|
|
209
212
|
const [busy, setBusy] = useState(false);
|
|
210
213
|
const [error, setError] = useState("");
|
|
@@ -21,6 +21,7 @@ register("settings", {
|
|
|
21
21
|
namedTunnelSetup: "How to set this up",
|
|
22
22
|
namedToken: "Cloudflare connector token",
|
|
23
23
|
namedTokenHint: "Stored privately, never shown again. Format checked locally; Cloudflare credentials and routing are checked when opening. Saving or clearing closes the current public link.",
|
|
24
|
+
namedTokenInvalid: "Paste only the long string that starts with eyJ.",
|
|
24
25
|
namedTokenSet: "Set token",
|
|
25
26
|
namedTokenClear: "Clear token",
|
|
26
27
|
awaitingConfirmation: "Waiting for confirmation.",
|
|
@@ -621,6 +622,7 @@ register("settings", {
|
|
|
621
622
|
namedTunnelSetup: "設定教學",
|
|
622
623
|
namedToken: "Cloudflare 連接器 token",
|
|
623
624
|
namedTokenHint: "私密儲存,不會讀回。儲存時只檢查格式,開啟時才確認憑證與路由。設定或清除會關閉目前的公開連結。",
|
|
625
|
+
namedTokenInvalid: "請只貼上以 eyJ 開頭的長字串。",
|
|
624
626
|
namedTokenSet: "設定 token",
|
|
625
627
|
namedTokenClear: "清除 token",
|
|
626
628
|
awaitingConfirmation: "等待確認中。",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// hands it back — afterwards a connection shows "Token set · @bot".
|
|
6
6
|
import { html, useState } from "/assets/app-html.js";
|
|
7
7
|
import { t } from "/assets/app-i18n.js";
|
|
8
|
+
import { useTypedText } from "/assets/text-undo.js";
|
|
8
9
|
import { Icon } from "/assets/ui-icons.js";
|
|
9
10
|
import { api } from "./settings-confirm.js";
|
|
10
11
|
|
|
@@ -36,10 +37,11 @@ export const botHandle = (username) => (username ? `@${String(username).replace(
|
|
|
36
37
|
*/
|
|
37
38
|
export function TokenField({ id, platform, value, onInput, identity, busy, onVerify, hint }) {
|
|
38
39
|
const [shown, setShown] = useState(false);
|
|
40
|
+
const [text, onText] = useTypedText(value); // #2342: the box keeps what was typed; the parent gets it trimmed
|
|
39
41
|
return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
|
|
40
42
|
<div class="token-row">
|
|
41
43
|
<input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
|
|
42
|
-
placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${
|
|
44
|
+
placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${text} onInput=${(e) => onInput(onText(e.target.value))} />
|
|
43
45
|
<button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
|
|
44
46
|
aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
|
|
45
47
|
</div>
|
|
@@ -5,10 +5,12 @@
|
|
|
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";
|
|
8
|
+
import { useTypedText } from "/assets/text-undo.js";
|
|
8
9
|
import { toast } from "/assets/ui-toast.js";
|
|
9
10
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
10
11
|
import { api } from "./settings-confirm.js";
|
|
11
12
|
import { Feedback } from "./settings-dialogs.js";
|
|
13
|
+
import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
|
|
12
14
|
import { resetWebAuthorization } from "/assets/web-authorize.js";
|
|
13
15
|
import "./settings-strings.js";
|
|
14
16
|
|
|
@@ -26,6 +28,8 @@ export function verifiedAuthorityBody(original, f) {
|
|
|
26
28
|
export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
27
29
|
const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
|
|
28
30
|
const [original, setOriginal] = useState(null), [f, setF] = useState(null), [msg, setMsg] = useState(null);
|
|
31
|
+
// #2342: the hours box keeps what was typed ("01", "1.50"; its undo history); the form gets the number.
|
|
32
|
+
const [hoursText, onHoursText] = useTypedText(f ? f.hours : null, (v) => (v.trim() === "" ? null : Number(v)));
|
|
29
33
|
const load = async () => {
|
|
30
34
|
// What changes here (a revoke, the switch) changes this browser's own line: the chat reads it again when it shows.
|
|
31
35
|
resetWebAuthorization();
|
|
@@ -46,7 +50,6 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
46
50
|
if (!overview || !f) return card(html`<p class="note">${t("app.loading")}</p>`);
|
|
47
51
|
|
|
48
52
|
const set = (patch) => setF((x) => ({ ...x, ...patch }));
|
|
49
|
-
const hoursText = f.hours === null ? "" : String(f.hours);
|
|
50
53
|
const hoursError = f.hours !== null && (!Number.isFinite(f.hours) || f.hours < overview.hours.min || f.hours > overview.hours.max) ? "vaHoursRange" : "";
|
|
51
54
|
const stuckOff = f.enabled && original.enabled && !overview.state.enabled;
|
|
52
55
|
const warnHosts = !f.loopbackOnly && overview.lanHosts.length > 0;
|
|
@@ -55,7 +58,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
55
58
|
const review = () => {
|
|
56
59
|
if (hoursError) return;
|
|
57
60
|
const body = verifiedAuthorityBody(original, f);
|
|
58
|
-
if (body) ctx.stage("verified_authority", { label: tn("vaTitle"), impact: "
|
|
61
|
+
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 } } });
|
|
59
62
|
else if (staged.has("verified_authority")) ctx.unstage("verified_authority");
|
|
60
63
|
setMsg(tn("readyApply"));
|
|
61
64
|
};
|
|
@@ -77,7 +80,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
|
|
|
77
80
|
${warnHosts ? html`<p class="note s-va-warn" role="status" id="vaLanWarning">${tn("vaLanWarning", overview.lanHosts.join(", "))}</p>` : null}
|
|
78
81
|
<div class="field"><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
|
|
79
82
|
<input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any" placeholder=${tn("defaultHours", overview.hours.default)}
|
|
80
|
-
class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours:
|
|
83
|
+
class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: onHoursText(e.target.value) })} />
|
|
81
84
|
<${Feedback} error=${hoursError} /></div>
|
|
82
85
|
<h4>${tn("vaSessionsTitle")}</h4>
|
|
83
86
|
${overview.authorized.length
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// progress, deadline and restart recovery as every other change.
|
|
5
5
|
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
6
6
|
import { t } from "/assets/app-i18n.js";
|
|
7
|
+
import { useTypedText } from "/assets/text-undo.js";
|
|
7
8
|
import { useLease } from "/assets/app-ctx.js";
|
|
8
9
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
9
10
|
import { api } from "./settings-confirm.js";
|
|
10
11
|
import { startOperation } from "./settings-apply.js";
|
|
11
|
-
import { BACKENDS,
|
|
12
|
+
import { BACKENDS, newConnectionImpact, toYaml } from "./settings-model.js";
|
|
12
13
|
import { Drawer, Select } from "./settings-dialogs.js";
|
|
13
14
|
import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
14
15
|
import { DiscordServerPicker } from "./settings-discord.js";
|
|
@@ -26,6 +27,11 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
26
27
|
const bump = () => { rev.current++; };
|
|
27
28
|
const [busy, setBusy] = useState("");
|
|
28
29
|
const [err, setErr] = useState("");
|
|
30
|
+
// #2342: the typed fields keep what was typed (a space in a path, the undo history); the form gets them trimmed.
|
|
31
|
+
const [dirText, onDirText] = useTypedText(w ? w.working_directory : "");
|
|
32
|
+
const [nameText, onNameText] = useTypedText(w ? w.instance_name : "");
|
|
33
|
+
const [userText, onUserText] = useTypedText(w ? w.admin_user_id : "");
|
|
34
|
+
const [groupText, onGroupText] = useTypedText(w ? w.group_id : "");
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
(async () => {
|
|
31
37
|
const env = await api("/api/settings/quickstart/environment").catch(() => null);
|
|
@@ -115,7 +121,7 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
115
121
|
// either if it was taken since, instead of quietly picking another.
|
|
116
122
|
const body = { ...input(), channel_id: w.plan.channel_id, token_env: w.plan.token_env, token_env_generated: true, token: w.token };
|
|
117
123
|
// It writes the connection and the first agent: it costs what a connection change does (the server's schema).
|
|
118
|
-
const handed = startOperation([{ label: tn("wizardTitle"), impact:
|
|
124
|
+
const handed = startOperation([{ label: tn("wizardTitle"), impact: newConnectionImpact(ctx && ctx.schema),
|
|
119
125
|
request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
|
|
120
126
|
if (!handed) { setErr(tn("applyBusyLocal")); return; }
|
|
121
127
|
setW((x) => ({ ...x, token: "" }));
|
|
@@ -134,8 +140,8 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
134
140
|
<p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
|
|
135
141
|
<${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
|
|
136
142
|
<p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
|
|
137
|
-
<div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${
|
|
138
|
-
<div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${
|
|
143
|
+
<div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${dirText} onInput=${(e) => set("working_directory")(onDirText(e.target.value))} /></div>
|
|
144
|
+
<div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${nameText} onInput=${(e) => set("instance_name")(onNameText(e.target.value))} /></div>`;
|
|
139
145
|
} else if (w.step === 2) {
|
|
140
146
|
body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
|
|
141
147
|
${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
|
|
@@ -148,12 +154,12 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
148
154
|
<${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
|
|
149
155
|
invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
|
|
150
156
|
guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
|
|
151
|
-
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("discordUserPlaceholder")} value=${
|
|
157
|
+
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("discordUserPlaceholder")} value=${userText} onInput=${(e) => set("admin_user_id")(onUserText(e.target.value))} /></div>`
|
|
152
158
|
: html`
|
|
153
|
-
<div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${
|
|
159
|
+
<div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${groupText} onInput=${(e) => set("group_id")(onGroupText(e.target.value))} />
|
|
154
160
|
<p class="note">${tn("wizardDetectHint")}</p></div>
|
|
155
161
|
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${detect}>${tn("wizardDetect")}</button></div>
|
|
156
|
-
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("telegramUserPlaceholder")} value=${
|
|
162
|
+
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("telegramUserPlaceholder")} value=${userText} onInput=${(e) => set("admin_user_id")(onUserText(e.target.value))} /></div>`}`;
|
|
157
163
|
} else {
|
|
158
164
|
body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
|
|
159
165
|
<p><strong>${tn("wizardWillWrite")}</strong></p>
|
|
@@ -94,7 +94,7 @@ register("app", {
|
|
|
94
94
|
search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
|
|
95
95
|
confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
|
|
96
96
|
// 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",
|
|
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",
|
|
98
98
|
},
|
|
99
99
|
"zh-TW": {
|
|
100
100
|
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
|
|
@@ -129,6 +129,6 @@ register("app", {
|
|
|
129
129
|
needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
|
|
130
130
|
search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
|
|
131
131
|
confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
|
|
132
|
-
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_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗",
|
|
132
|
+
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_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
|
|
133
133
|
},
|
|
134
134
|
});
|
|
@@ -77,8 +77,8 @@ export function startNeedsNotifier({ open, env = globalThis } = {}) {
|
|
|
77
77
|
seen.add(item.id);
|
|
78
78
|
if (!notifyOn(env)) return;
|
|
79
79
|
try {
|
|
80
|
-
const n = new env.Notification(item.instance, { body: item.detail ? `${reasonText(item)}: ${item.detail}` : reasonText(item), tag: item.id });
|
|
81
|
-
n.onclick = () => { try { env.focus && env.focus(); } catch { /* ignore */ } if (open) open(item.instance); n.close(); };
|
|
80
|
+
const n = new env.Notification(item.instance || reasonText(item), { body: item.detail ? `${reasonText(item)}: ${item.detail}` : reasonText(item), tag: item.id });
|
|
81
|
+
n.onclick = () => { try { env.focus && env.focus(); } catch { /* ignore */ } if (open && item.instance) open(item.instance); n.close(); };
|
|
82
82
|
} catch { /* a browser that refuses at the last moment: the list still shows it */ }
|
|
83
83
|
};
|
|
84
84
|
const consider = (items) => {
|
|
@@ -24,7 +24,7 @@ export function createStore(initial) {
|
|
|
24
24
|
* (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
|
|
25
25
|
* `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
|
|
26
26
|
export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
|
|
27
|
-
hydration: "none", retryHydration: null });
|
|
27
|
+
hydration: "none", retryHydration: null, canvasWorker: typeof document !== "undefined" && document.body?.dataset.canvasWorker === "1" });
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* #1920 phase 3: this page is a guest's — the server says so on <body> (data-tier="guest"). A guest sees only its own
|
|
@@ -49,7 +49,7 @@ export function applyStatus(d) {
|
|
|
49
49
|
exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
|
|
50
50
|
if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
|
|
51
51
|
}
|
|
52
|
-
appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
|
|
52
|
+
appStore.set({ canvasWorker: d.canvas_worker === true, ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
|
|
53
53
|
connections: typeof d.connections === "number" ? d.connections : null,
|
|
54
54
|
// #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
|
|
55
55
|
prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -383,6 +383,17 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
383
383
|
.dlg-title { flex: 1; font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
|
|
384
384
|
.dlg-body { padding: var(--s-2) var(--s-5) var(--s-4); overflow-y: auto; display: flex; flex-direction: column; gap: var(--s-3); }
|
|
385
385
|
.dlg-actions { display: flex; justify-content: flex-end; gap: var(--s-2); padding: var(--s-3) var(--s-5) var(--s-5); }
|
|
386
|
+
/* A drawer (ui-dialog.js `drawer`): on a desk, the right edge at full height, the page in view beside it. A phone keeps
|
|
387
|
+
the bottom sheet (the rules below are for 900 px and up only). */
|
|
388
|
+
@media (min-width: 900px) {
|
|
389
|
+
.dlg.dlg-drawer { width: min(620px, 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0 0 0 auto; border-radius: 0;
|
|
390
|
+
background: var(--bg); box-shadow: var(--shadow-2); animation: dlg-drawer-in 0.18s var(--ease); }
|
|
391
|
+
.dlg-drawer .dlg-box { height: 100%; max-height: 100dvh; }
|
|
392
|
+
.dlg-drawer .dlg-head { padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5); border-bottom: 1px solid var(--border); }
|
|
393
|
+
.dlg-drawer .dlg-body { flex: 1; padding-top: var(--s-4); padding-bottom: var(--s-8); }
|
|
394
|
+
}
|
|
395
|
+
@keyframes dlg-drawer-in { from { transform: translateX(28px); opacity: 0.6; } }
|
|
396
|
+
@media (prefers-reduced-motion: reduce) { .dlg.dlg-drawer { animation: none; } }
|
|
386
397
|
|
|
387
398
|
/* ── Forms ── */
|
|
388
399
|
.form { display: flex; flex-direction: column; gap: var(--s-3); }
|
|
@@ -514,8 +525,19 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
514
525
|
.lb { position: fixed; inset: 0; width: 100vw; height: 100vh; height: 100dvh; max-width: none; max-height: none; margin: 0; padding: 0; border: 0;
|
|
515
526
|
background: rgba(0, 0, 0, 0.95); color: #fff; overflow: hidden; overscroll-behavior: contain; }
|
|
516
527
|
.lb::backdrop { background: rgba(0, 0, 0, 0.6); }
|
|
517
|
-
.lb-stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 56px 16px 16px; box-sizing: border-box;
|
|
518
|
-
|
|
528
|
+
.lb-stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 56px 16px 16px; box-sizing: border-box;
|
|
529
|
+
overflow: hidden; touch-action: none; }
|
|
530
|
+
/* #2333: the picture at its own size, placed and scaled by a CSSOM transform from its top-left corner (image-zoom.js);
|
|
531
|
+
hidden until it has loaded and been fitted. One that fails to load is shown where the browser puts a broken one. */
|
|
532
|
+
.lb-img { position: absolute; left: 0; top: 0; max-width: none; max-height: none; display: block; transform-origin: 0 0; will-change: transform;
|
|
533
|
+
visibility: hidden; cursor: zoom-in; user-select: none; -webkit-user-select: none; -webkit-user-drag: none; -webkit-touch-callout: none; }
|
|
534
|
+
.lb-img.lb-ready { visibility: visible; }
|
|
535
|
+
.lb-img.lb-err { position: static; max-width: 100%; max-height: 100%; visibility: visible; transform: none; cursor: default; }
|
|
536
|
+
.lb-stage.lb-zoomed .lb-img { cursor: grab; }
|
|
537
|
+
.lb-stage.lb-zoomed .lb-img:active { cursor: grabbing; }
|
|
538
|
+
.lb-zoom { display: inline-flex; align-items: center; gap: 2px; }
|
|
539
|
+
.lb .lb-level { min-width: 4.5em; justify-content: center; color: #fff; background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.3); font-variant-numeric: tabular-nums; }
|
|
540
|
+
.lb .lb-fit-width.on { background: rgba(255, 255, 255, 0.22); }
|
|
519
541
|
.lb-bar { position: absolute; top: 0; left: 0; right: 0; z-index: 1; display: flex; align-items: center; gap: var(--s-2); padding: var(--s-2) var(--s-3);
|
|
520
542
|
background: rgba(0, 0, 0, 0.75); }
|
|
521
543
|
.lb-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); }
|
|
@@ -1920,19 +1942,22 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1920
1942
|
.tb-count { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text-3); }
|
|
1921
1943
|
.tb-drop-line { height: 3px; border-radius: 2px; background: var(--accent); }
|
|
1922
1944
|
|
|
1923
|
-
.tb-tile { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-3) var(--s-3) var(--s-3) calc(var(--s-3) + 2px); border-radius: var(--r-md); background: var(--bg); border: 1px solid var(--border);
|
|
1924
|
-
.tb-tile:hover { border-color: var(--border-strong); }
|
|
1945
|
+
.tb-tile { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-3) var(--s-3) var(--s-3) calc(var(--s-3) + 2px); border-radius: var(--r-md); background: var(--bg); border: 1px solid var(--border); box-shadow: var(--shadow-1); --lbar-color: transparent; cursor: pointer; font-size: var(--fs-sm); min-width: 0; }
|
|
1946
|
+
.tb-tile:hover { border-color: var(--border-strong); box-shadow: var(--shadow-2); }
|
|
1947
|
+
/* As taskhall: the shared straight bar (#2330) only while the card is under way (in progress, blocked, waiting for
|
|
1948
|
+
you); none on to do or done, and a done tile is dimmed. */
|
|
1925
1949
|
.tb-tile.st-claimed { --lbar-color: var(--accent); }
|
|
1926
1950
|
.tb-tile.st-blocked { --lbar-color: var(--danger); }
|
|
1927
1951
|
.tb-tile.st-review { --lbar-color: var(--warn); }
|
|
1928
|
-
.tb-tile.st-done {
|
|
1952
|
+
.tb-tile.st-done { background: color-mix(in srgb, var(--bg) 60%, var(--surface)); box-shadow: none; }
|
|
1953
|
+
.tb-tile.st-done .tb-tile-title { color: var(--text-2); }
|
|
1929
1954
|
.tb-tile.dragging { opacity: 0.4; }
|
|
1930
1955
|
.tb-tile.flash { animation: tb-flash 1.6s var(--ease); }
|
|
1931
1956
|
@keyframes tb-flash { from { box-shadow: 0 0 0 3px var(--accent-dim); background: var(--accent-dim); } to { box-shadow: none; } }
|
|
1932
1957
|
.tb-tile-top { display: flex; align-items: center; gap: var(--s-2); }
|
|
1933
|
-
.tb-size { font-size: var(--
|
|
1958
|
+
.tb-size { padding: 0 5px; border: 1px solid var(--border); border-radius: var(--r-sm); font-size: 11px; font-weight: var(--fw-bold); line-height: 16px; color: var(--text-3); }
|
|
1934
1959
|
/* The tile's control: its title, a plain-looking button (Enter opens the card, Alt+arrows move it). */
|
|
1935
|
-
.tb-tile-title { display: block; width: 100%; padding: 0; background: none; border: 0; border-radius: var(--r-sm); color: var(--text); font: inherit; font-weight: var(--fw-
|
|
1960
|
+
.tb-tile-title { display: block; width: 100%; padding: 0; background: none; border: 0; border-radius: var(--r-sm); color: var(--text); font: inherit; font-weight: var(--fw-semibold); line-height: 1.4; text-align: left; cursor: pointer; overflow-wrap: anywhere; }
|
|
1936
1961
|
.tb-tile-title:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
1937
1962
|
.tb-tile:has(.tb-tile-title:focus-visible) { border-color: var(--focus); }
|
|
1938
1963
|
.tb-tile-note { display: flex; gap: var(--s-1); align-items: flex-start; font-size: var(--fs-xs); color: var(--text-2); overflow-wrap: anywhere; }
|
|
@@ -1959,12 +1984,47 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1959
1984
|
.tb-sub-blocked { padding-left: 22px; font-size: var(--fs-xs); color: var(--danger); overflow-wrap: anywhere; }
|
|
1960
1985
|
.tb-sub-more { font-size: var(--fs-xs); padding-left: var(--s-1); }
|
|
1961
1986
|
.tb-tile-foot { display: flex; align-items: center; gap: var(--s-2); }
|
|
1962
|
-
.tb-tile-foot > .tb-who { flex: 1; min-width: 0; max-width: none; }
|
|
1987
|
+
.tb-tile-foot > .tb-who, .tb-tile-foot > .tb-person, .tb-tile-foot > .tb-people { flex: 1; min-width: 0; max-width: none; }
|
|
1988
|
+
.tb-people { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
|
|
1989
|
+
/* Who (fleet-task-common.js): a round initial — the instance's identity colour when it has one — then the name. */
|
|
1990
|
+
.tb-person { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
|
|
1991
|
+
.tb-person > .tb-who { min-width: 0; }
|
|
1992
|
+
.tb-person.none > .tb-who { color: var(--text-3); }
|
|
1993
|
+
.tb-av { display: inline-grid; place-items: center; width: 20px; height: 20px; flex: none; border-radius: 50%; font-size: 11px; font-weight: var(--fw-bold); line-height: 1;
|
|
1994
|
+
background: color-mix(in srgb, var(--id, var(--text-3)) 16%, var(--bg)); color: var(--id-name, var(--text-2)); }
|
|
1963
1995
|
.tb-tile-stats { display: inline-flex; gap: var(--s-2); font-size: var(--fs-xs); color: var(--text-3); }
|
|
1964
1996
|
.tb-tile-stats > span { display: inline-flex; align-items: center; gap: 2px; }
|
|
1965
1997
|
.tb-tile-stats > .all { color: var(--ok); }
|
|
1966
1998
|
.tb-tile-stats > .reply { color: var(--accent); }
|
|
1967
1999
|
|
|
2000
|
+
/* #2246 step 3: who works on what (fleet-task-work.js) — the board's Instance drawer and the Details page's block. */
|
|
2001
|
+
.tw { display: flex; flex-direction: column; gap: var(--s-4); min-width: 0; }
|
|
2002
|
+
.tw-head { margin: 0; font-size: var(--fs-sm); }
|
|
2003
|
+
.tw-error { margin: 0; color: var(--danger); font-size: var(--fs-sm); }
|
|
2004
|
+
.tw-group { display: flex; flex-direction: column; gap: var(--s-2); }
|
|
2005
|
+
.tw-group-head { display: flex; align-items: center; gap: var(--s-2); margin: 0; font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-2); }
|
|
2006
|
+
.tw-inst { display: flex; flex-direction: column; gap: var(--s-1); padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--bg); min-width: 0; }
|
|
2007
|
+
.tw-inst-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
|
|
2008
|
+
.tw-inst-head .av { width: 24px; height: 24px; font-size: 11px; }
|
|
2009
|
+
.tw-inst-head .av .av-dot { box-shadow: 0 0 0 2px var(--bg); }
|
|
2010
|
+
.tw-name { font-weight: var(--fw-semibold); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
2011
|
+
.tw-state { font-size: var(--fs-xs); white-space: nowrap; }
|
|
2012
|
+
.tw-alt { font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
2013
|
+
.tw-desc { margin: 0; font-size: var(--fs-xs); overflow-wrap: anywhere; }
|
|
2014
|
+
.tw-none { margin: 0; font-size: var(--fs-xs); }
|
|
2015
|
+
.tw-lines { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
|
|
2016
|
+
.tw-line { display: flex; align-items: baseline; gap: var(--s-2); width: 100%; padding: 2px var(--s-1); background: none; border: 0; border-radius: var(--r-sm); color: var(--text); text-align: left; cursor: pointer; font-size: var(--fs-sm); min-width: 0; }
|
|
2017
|
+
.tw-line:hover { background: var(--surface-2); }
|
|
2018
|
+
.tw-line:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
2019
|
+
.tw-kind { flex-shrink: 0; min-width: 4.5em; font-size: 11px; font-weight: var(--fw-semibold); color: var(--text-2); }
|
|
2020
|
+
.tw-kind.k-doing { color: var(--accent); }
|
|
2021
|
+
.tw-kind.k-blocked, .tw-kind.k-back { color: var(--danger); }
|
|
2022
|
+
.tw-kind.k-review { color: var(--warn); }
|
|
2023
|
+
.tw-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2024
|
+
.tw-note { flex-shrink: 0; font-size: var(--fs-xs); white-space: nowrap; }
|
|
2025
|
+
.tw-unread { flex-shrink: 0; display: inline-flex; color: var(--accent); }
|
|
2026
|
+
.d-board .tw-lines { margin-top: var(--s-1); }
|
|
2027
|
+
|
|
1968
2028
|
/* One card, in the app's dialog. */
|
|
1969
2029
|
.tc { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; }
|
|
1970
2030
|
.tc-head { display: flex; align-items: center; gap: var(--s-2); }
|
|
@@ -1974,7 +2034,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1974
2034
|
.tc-ver { font-size: var(--fs-xs); }
|
|
1975
2035
|
.tc-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); padding-bottom: var(--s-3); border-bottom: 1px solid var(--border); }
|
|
1976
2036
|
.tc-hint { font-size: var(--fs-xs); }
|
|
1977
|
-
.tc-meta { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-2) var(--s-4); margin: 0; font-size: var(--fs-sm); align-items: center; }
|
|
2037
|
+
.tc-meta { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-2) var(--s-4); margin: 0; padding: var(--s-3) var(--s-4); border-radius: var(--r-md); background: var(--surface); font-size: var(--fs-sm); align-items: center; }
|
|
1978
2038
|
.tc-meta dt { color: var(--text-2); }
|
|
1979
2039
|
.tc-meta dd { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: 0; min-width: 0; }
|
|
1980
2040
|
.tc-value { color: var(--text); overflow-wrap: anywhere; }
|
|
@@ -2012,6 +2072,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
2012
2072
|
.tb-col { min-height: 0; }
|
|
2013
2073
|
.tb-form-row { grid-template-columns: minmax(0, 1fr); }
|
|
2014
2074
|
.tb-search { flex: 1 1 100%; }
|
|
2075
|
+
/* The card's sheet keeps its fields full width (the grey block is the desk drawer's). */
|
|
2076
|
+
.tc-meta { padding: 0; background: none; }
|
|
2015
2077
|
/* #2299: a phone's waiting row: pill and title on one line, the actions under them (assignee left, buttons right). */
|
|
2016
2078
|
.tb-wait-main > .tb-pill { min-width: 0; }
|
|
2017
2079
|
.tb-wait-actions { flex-basis: 100%; margin-left: 0; }
|
|
@@ -2022,6 +2084,15 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
2022
2084
|
/* …and they do not pile down the board: an older confirmation gives way to two newer toasts; an error never does. */
|
|
2023
2085
|
.shell:has(.tb-home, .tb-board-page) .toasts > .toast.ok:nth-last-child(n+3) { display: none; }
|
|
2024
2086
|
}
|
|
2087
|
+
/* On a desk a project's columns reach the bottom of the page, as taskhall's: a card drops anywhere down a column, and
|
|
2088
|
+
an empty column still reads as one. The page's column, the board and its scroller pass the height down as flex. */
|
|
2089
|
+
@media (min-width: 900px) {
|
|
2090
|
+
.col:has(> .tb > .tb-board-page) { display: flex; flex-direction: column; min-height: 100%; }
|
|
2091
|
+
.tb:has(> .tb-board-page) { flex: 1; }
|
|
2092
|
+
.tb-board-page { display: flex; flex-direction: column; flex: 1; }
|
|
2093
|
+
.tb-board-page > .tb-scroll { display: flex; flex-direction: column; flex: 1; }
|
|
2094
|
+
.tb-board-page .tb-columns { flex: 1; align-items: stretch; }
|
|
2095
|
+
}
|
|
2025
2096
|
@media (prefers-reduced-motion: reduce) {
|
|
2026
2097
|
.tb-tile.flash { animation: none; }
|
|
2027
2098
|
.tb-chev { transition: none; }
|
package/dist/ui/shared/app.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// #1665 first: the host's preferences go into this browser's cache before any module below reads its key.
|
|
9
9
|
import { bootPrefs } from "./app-prefs.js";
|
|
10
10
|
import { html, render } from "./app-html.js";
|
|
11
|
-
import { Shell, handleKey, handleSearchKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener } from "./app-shell.js";
|
|
11
|
+
import { Shell, handleKey, handleSearchKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener, isMacLike } from "./app-shell.js";
|
|
12
12
|
import { installSwipe } from "./app-swipe.js";
|
|
13
|
+
import { installTextUndo } from "./text-undo.js";
|
|
13
14
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
14
15
|
import { createStream } from "./app-stream.js";
|
|
15
16
|
import { appStore, applyStatus, applyActivity, guestPage } from "./app-store.js";
|
|
@@ -125,6 +126,9 @@ if (!guest) installOwnerCss();
|
|
|
125
126
|
document.addEventListener("keydown", handleKey);
|
|
126
127
|
// #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app only; the server refuses a guest's search anyway.
|
|
127
128
|
if (mode === "full" && !guest) document.addEventListener("keydown", handleSearchKey);
|
|
129
|
+
// #2342: Ctrl/⌘+Z and Ctrl+Shift+Z / Ctrl+Y / ⌘⇧Z in every text field, one history per field (the browser's is one
|
|
130
|
+
// for the whole page, and any script write wipes it).
|
|
131
|
+
installTextUndo({ mac: isMacLike() });
|
|
128
132
|
// #1605: on a phone (the sidebar is a drawer), swipe right in the main area to open it, left to close it — the same
|
|
129
133
|
// open/close as ☰ and the scrim. Nothing that owns a sideways drag (code, tables, the terminal, inputs, dialogs) and
|
|
130
134
|
// never the left edge (iOS: "back").
|
|
@@ -13,7 +13,7 @@ const episodes = new WeakSet();
|
|
|
13
13
|
|
|
14
14
|
/** `current` belongs to the page/session episode. Route cleanup only releases its contribution. */
|
|
15
15
|
export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
|
|
16
|
-
let dead = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
|
|
16
|
+
let dead = false, claimed = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
|
|
17
17
|
const bindings = new Map(), profiles = new Map();
|
|
18
18
|
let lastContext = null, playbackUnavailable = null;
|
|
19
19
|
const identity = effectPlaybackIdentity(host), auth = host?.AgendAuth;
|
|
@@ -26,7 +26,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
26
26
|
};
|
|
27
27
|
const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
|
|
28
28
|
const clock = () => host.performance.now();
|
|
29
|
-
let offAuth = null;
|
|
29
|
+
let offAuth = null, disposed = null;
|
|
30
30
|
const snapshot = s => {
|
|
31
31
|
const effects = s?.metadata?.effects ?? [], known = registry?.snapshot?.() ?? [];
|
|
32
32
|
const rows = effects.map(p => {
|
|
@@ -76,16 +76,18 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
76
76
|
if (pending?.source === s) safe(() => pending.controller.abort());
|
|
77
77
|
revision++; stamp = null;
|
|
78
78
|
}
|
|
79
|
-
function dispose() {
|
|
80
|
-
if (dead) return; dead = true; discard(source);
|
|
79
|
+
function dispose(restartable = false) {
|
|
80
|
+
if (dead) return disposed; dead = true; discard(source);
|
|
81
81
|
safe(() => trial?.dispose()); safe(() => pending?.controller.abort()); safe(() => registry?.dispose?.()); safe(() => loader?.dispose?.()); safe(() => personal?.dispose()); safe(() => writer?.dispose()); safe(() => offAuth?.());
|
|
82
82
|
safe(() => host?.removeEventListener?.("pagehide", dispose));
|
|
83
83
|
for (const row of profiles.values()) safe(() => row.collector.dispose()); profiles.clear(); bindings.clear(); safe(() => onDispose({ error }));
|
|
84
|
+
disposed = Promise.all([safe(() => loader?.whenDisposed?.()), safe(() => registry?.whenDisposed?.())]).then(() => { if (restartable === true && claimed) episodes.delete(host); });
|
|
85
|
+
return disposed;
|
|
84
86
|
}
|
|
85
87
|
function fail(reason) { if (dead) return; error = reason; dispose(); safe(() => onChange({ error, rows: [], effects: [], loading: false })); }
|
|
86
88
|
const unavailable = { ok: false, sync() {}, changedChat() {}, reply() {}, detachChat() {}, attachChat() { return null; }, attachTrial() { return null; }, stopTrial() {}, trialSnapshot: () => ({ rows: [] }), snapshot: () => snapshot(null), dispose };
|
|
87
89
|
if (!host || typeof host !== "object" || episodes.has(host) || !identity || typeof current !== "function" || typeof observe !== "function" || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function") return unavailable;
|
|
88
|
-
episodes.add(host); // Claim before invoking current/auth callbacks: one attempt per Window episode.
|
|
90
|
+
episodes.add(host); claimed = true; // Claim before invoking current/auth callbacks: one attempt per Window episode.
|
|
89
91
|
if (!alive()) { dispose(); return unavailable; }
|
|
90
92
|
try {
|
|
91
93
|
offAuth = auth.onSessionEnd(dispose);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chatEffectIdentity } from "./effect-display-metadata.js";
|
|
1
2
|
/** #1629: one committed local Chat lease joins the reviewed host-only effect primitives. */
|
|
2
3
|
import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
|
|
3
4
|
import { createEffectRegistry } from "./effect-registry.js";
|
|
@@ -43,13 +44,7 @@ export function createEffectWriter(host, current) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** Frozen, server-rendered session identity only. A header-token/public/View load has no identity. */
|
|
46
|
-
export
|
|
47
|
-
try {
|
|
48
|
-
const b = host.document.body.dataset, x = JSON.parse(host.document.documentElement.getAttribute("data-prefs") || "null");
|
|
49
|
-
if (b.mode !== "full" || b.tier === "guest" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
|
|
50
|
-
return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
|
|
51
|
-
} catch { return null; }
|
|
52
|
-
}
|
|
47
|
+
export { chatEffectIdentity } from "./effect-display-metadata.js";
|
|
53
48
|
/** Chat-only public/guest capability stays separate from personal preferences and management. */
|
|
54
49
|
export function effectPlaybackIdentity(host) {
|
|
55
50
|
try {
|
|
@@ -25,9 +25,10 @@ export function createEffectCursor({ window: host, current, readImage, onChange
|
|
|
25
25
|
const unavailable = { ok: false, update() {}, snapshot: () => ({ status: "unavailable", id: null }), dispose() {} };
|
|
26
26
|
if (!host?.document || owners.has(host) || ![current, readImage, onChange, host.getComputedStyle, host.Blob, host.URL?.createObjectURL, host.URL?.revokeObjectURL, host.AbortController, host.setTimeout, host.clearTimeout, host.performance?.now].every(x => typeof x === "function")) return unavailable;
|
|
27
27
|
const token = {}; owners.set(host, token);
|
|
28
|
+
let finishDisposed; const disposed = new Promise(resolve => { finishDisposed = resolve; });
|
|
28
29
|
const doc = host.document; let dead = false, previous = null, wanted = null, pending = null, active = null, blocked = null, generation = 0, checking = false, notifying = false, status = "off";
|
|
29
30
|
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
30
|
-
const release = () => { if (dead && !pending
|
|
31
|
+
const release = () => { if (dead && !pending) { if (owners.get(host) === token) owners.delete(host); finishDisposed(); } };
|
|
31
32
|
function alive() {
|
|
32
33
|
if (dead || checking) return false;
|
|
33
34
|
checking = true; const value = safe(() => current() === true && host.document === doc); checking = false;
|
|
@@ -121,5 +122,5 @@ export function createEffectCursor({ window: host, current, readImage, onChange
|
|
|
121
122
|
}
|
|
122
123
|
start(); tell();
|
|
123
124
|
}
|
|
124
|
-
return { ok: true, update, snapshot, dispose };
|
|
125
|
+
return { ok: true, update, snapshot, dispose, whenDisposed: () => disposed };
|
|
125
126
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** #1629: closed host display metadata. Neither this object nor its token grants a trial read. */
|
|
2
|
+
const KEYS = ["v", "offer", "id", "title", "expiresAt"], ID = /^[0-9a-f]{32}$/;
|
|
3
|
+
const FORBIDDEN = /(?!\u200d)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
4
|
+
const VISIBLE = /[^\p{M}\p{C}\p{Z}\u115f\u1160\u3164\uffa0\u2800]/u;
|
|
5
|
+
let graphemes = null;
|
|
6
|
+
export function checkEffectTrialOffer(value) {
|
|
7
|
+
try {
|
|
8
|
+
if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
|
|
9
|
+
const keys = Reflect.ownKeys(value);
|
|
10
|
+
if (keys.length !== KEYS.length || keys.some(k => typeof k !== "string" || !KEYS.includes(k))) return null;
|
|
11
|
+
const x = Object.create(null);
|
|
12
|
+
for (const key of KEYS) {
|
|
13
|
+
const d = Object.getOwnPropertyDescriptor(value, key);
|
|
14
|
+
if (!d || !("value" in d) || !d.enumerable) return null;
|
|
15
|
+
x[key] = d.value;
|
|
16
|
+
}
|
|
17
|
+
if (x.v !== 1 || typeof x.offer !== "string" || !ID.test(x.offer) || typeof x.id !== "string" || !ID.test(x.id)
|
|
18
|
+
|| typeof x.title !== "string" || x.title.length > 400 || x.title.normalize("NFC") !== x.title || x.title.trim() !== x.title
|
|
19
|
+
|| FORBIDDEN.test(x.title) || !VISIBLE.test(x.title) || !Number.isSafeInteger(x.expiresAt) || x.expiresAt < 0) return null;
|
|
20
|
+
graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
21
|
+
let count = 0; for (const _part of graphemes.segment(x.title)) if (++count > 40) return null;
|
|
22
|
+
if (count < 1) return null;
|
|
23
|
+
return Object.freeze({ v: 1, offer: x.offer, id: x.id, title: x.title, expiresAt: x.expiresAt });
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
/** Role and sender are host fields, never inferred from a message's text. Start must separately authorize. */
|
|
27
|
+
export function effectTrialMessage(message) {
|
|
28
|
+
if (!message || message.role !== "status" || message.sender !== "agend" || typeof message.instance !== "string"
|
|
29
|
+
|| message.instance.length < 1 || message.instance.length > 128) return null;
|
|
30
|
+
return checkEffectTrialOffer(message.effectTrial);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function chatEffectIdentity(host) {
|
|
34
|
+
try {
|
|
35
|
+
const b = host.document.body.dataset, x = JSON.parse(host.document.documentElement.getAttribute("data-prefs") || "null");
|
|
36
|
+
if (b.mode !== "full" || b.tier === "guest" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
|
|
37
|
+
return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
@@ -42,6 +42,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
42
42
|
try { if (!host?.document || !host.document.body || registries.has(host) || typeof load !== "function" || typeof stockSnapshot !== "function" || typeof onChange !== "function" || typeof onProfile !== "function" || typeof onMount !== "function" || typeof onRetire !== "function" || typeof clock !== "function" || !safety || ["allowed", "begin", "finish", "pause", "resume"].some(k => typeof safety[k] !== "function") || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout", "MutationObserver", "ResizeObserver", "AbortController"].some(k => typeof host[k] !== "function")) return { ok: false, reason: "registry_unsupported" }; } catch { return { ok: false, reason: "registry_unsupported" }; }
|
|
43
43
|
let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [], raf = null, timer = null;
|
|
44
44
|
const slots = new Map(), loads = new Set(), paused = new Map(), offs = [], observed = new Set(), failures = new Map(), results = new Map();
|
|
45
|
+
let finishDisposed; const disposed = new Promise(resolve => { finishDisposed = resolve; });
|
|
45
46
|
let readingStock = false, stockFailure = "stock_evidence_invalid", warned = null;
|
|
46
47
|
let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
|
|
47
48
|
const time = () => {
|
|
@@ -342,7 +343,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
342
343
|
}).catch(() => { if (current(s)) retire(s, "resource_unavailable"); }).finally(() => {
|
|
343
344
|
if (request.timer !== null) host.clearTimeout(request.timer);
|
|
344
345
|
loads.delete(request); if (s.request === request) s.request = null;
|
|
345
|
-
if (!dead) reconcile(); else if (loads.size === 0
|
|
346
|
+
if (!dead) reconcile(); else if (loads.size === 0) { if (registries.get(host) === api) registries.delete(host); finishDisposed(); }
|
|
346
347
|
});
|
|
347
348
|
}
|
|
348
349
|
schedule(); tell();
|
|
@@ -530,11 +531,12 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
530
531
|
}
|
|
531
532
|
return sent;
|
|
532
533
|
},
|
|
534
|
+
whenDisposed: () => disposed,
|
|
533
535
|
dispose() {
|
|
534
536
|
if (dead) return; dead = true; revision++; stopAll("disposed");
|
|
535
537
|
if (raf !== null) host.cancelAnimationFrame(raf); if (timer !== null) host.clearTimeout(timer);
|
|
536
538
|
observer.disconnect(); resize.disconnect(); longTasks?.disconnect(); for (const off of offs) off();
|
|
537
|
-
geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0
|
|
539
|
+
geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0) { if (registries.get(host) === api) registries.delete(host); finishDisposed(); }
|
|
538
540
|
},
|
|
539
541
|
};
|
|
540
542
|
// Read-only host geometry seam; no selector guessing among poster and live roots.
|