@songsid/agend 2.3.2-beta.3 → 2.3.2-beta.5
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 +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +45 -0
- package/dist/backend/kiro-identity.js +74 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +36 -1
- package/dist/backend/kiro.js +131 -6
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +6 -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.js +7 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +7 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +5 -0
- package/dist/classic-channel-manager.js +14 -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.js +22 -5
- 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.js +22 -7
- 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/fleet-manager.d.ts +51 -1
- package/dist/fleet-manager.js +416 -32
- package/dist/fleet-manager.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 +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +6 -3
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +12 -5
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +17 -0
- package/dist/instance-lifecycle.js +32 -4
- 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-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 +72 -8
- 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/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 +15 -0
- package/dist/notice-delivery.js +37 -0
- package/dist/notice-delivery.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 +24 -0
- package/dist/outbound-schemas.js +21 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- 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 +4 -1
- package/dist/quickstart.js.map +1 -1
- 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.js +27 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +17 -14
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -1
- 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 +7 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +126 -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 +4 -0
- package/dist/setup-wizard.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 +87 -40
- package/dist/transcript-sources.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +1 -1
- package/dist/turn-reply-guard.js +1 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +19 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/fleet-office-editor.js +10 -6
- package/dist/ui/fleet-office-layout.js +34 -28
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +2 -2
- package/dist/ui/fleet-office-pack.d.ts +3 -1
- package/dist/ui/fleet-office-pack.js +58 -7
- package/dist/ui/fleet-office-skin.js +13 -4
- package/dist/ui/fleet-office-store.js +27 -1
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +155 -6
- package/dist/ui/fleet-office.js +169 -41
- package/dist/ui/fleet-usage.js +19 -15
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +4 -4
- package/dist/ui/panel-settings.js +32 -10
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-model.js +14 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +14 -0
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +18 -2
- package/dist/ui/shared/side-panel.js +95 -0
- 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 +4 -0
- package/dist/web-api.js +110 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- 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-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 +1 -1
- package/dist/web-office-skins.js +1 -1
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/package.json +2 -1
- package/templates/windows-shortcuts.ps1 +18 -7
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// session that started a sign-in gets its terminal link, access code and device code; every other admin page only that
|
|
6
6
|
// one runs, on which device, and may cancel it. The stream's `backend_login` event carries nothing — it only says to
|
|
7
7
|
// read again. Starting is two steps, as in chat: check (the server keeps what it found) and start (with its id).
|
|
8
|
-
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
8
|
+
import { html, useEffect, useState, useRef } from "/assets/app-html.js";
|
|
9
9
|
import { t, register } from "/assets/app-i18n.js";
|
|
10
10
|
import { appStore, createStore, useStore } from "/assets/app-store.js";
|
|
11
11
|
import { closeDialog, shellStore, showDialog } from "/assets/app-shell.js";
|
|
12
12
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
13
13
|
import { toast } from "/assets/ui-toast.js";
|
|
14
|
+
import { mountStructuredSignIns } from "./structured-signin.js";
|
|
14
15
|
|
|
15
16
|
register("signin", {
|
|
16
17
|
en: {
|
|
@@ -49,6 +50,8 @@ register("signin", {
|
|
|
49
50
|
notOffered: "A public link is not offered for this sign-in.",
|
|
50
51
|
failed: "That did not work.",
|
|
51
52
|
listTitle: "Sign-ins",
|
|
53
|
+
browserRunning: "Signing in to {0} on this machine",
|
|
54
|
+
consoleFallback: "Use console fallback",
|
|
52
55
|
listHint: "Each backend's login on this machine, and each credential profile an agent uses.",
|
|
53
56
|
stUsable: "signed in",
|
|
54
57
|
stNotSignedIn: "not signed in",
|
|
@@ -98,6 +101,8 @@ register("signin", {
|
|
|
98
101
|
notOffered: "這次登入不提供公開連結。",
|
|
99
102
|
failed: "沒有成功。",
|
|
100
103
|
listTitle: "登入",
|
|
104
|
+
browserRunning: "正在這台機器登入 {0}",
|
|
105
|
+
consoleFallback: "改用主控台備援",
|
|
101
106
|
listHint: "這台機器上每個 backend 的登入,以及代理使用的每個憑證設定檔。",
|
|
102
107
|
stUsable: "已登入",
|
|
103
108
|
stNotSignedIn: "未登入",
|
|
@@ -116,7 +121,7 @@ const tn = (k, ...v) => t(`signin.${k}`, ...v);
|
|
|
116
121
|
const shownOf = (backend, profile) => (profile ? tn("profile", backend, profile) : backend);
|
|
117
122
|
|
|
118
123
|
/** What this page knows: the current sign-in as this session may see it, and Settings' list. */
|
|
119
|
-
export const signInStore = createStore({ loaded: false, login: null, targets: [] });
|
|
124
|
+
export const signInStore = createStore({ loaded: false, login: null, targets: [], structured: null });
|
|
120
125
|
|
|
121
126
|
let reading = null, again = false;
|
|
122
127
|
/**
|
|
@@ -125,9 +130,13 @@ let reading = null, again = false;
|
|
|
125
130
|
*/
|
|
126
131
|
export function refreshSignIn() {
|
|
127
132
|
if (reading) { again = true; return reading; }
|
|
128
|
-
reading =
|
|
129
|
-
.then(
|
|
130
|
-
|
|
133
|
+
reading = Promise.all([
|
|
134
|
+
fetch("/ui/backend-login", { cache: "no-store" }).then(r => r.ok ? r.json() : null).catch(() => null),
|
|
135
|
+
fetch("/ui/browser-backend-signins", { cache: "no-store" }).then(r => r.ok ? r.json() : null).catch(() => null),
|
|
136
|
+
]).then(([b, structured]) => {
|
|
137
|
+
if (b) signInStore.set({ loaded: true, login: b.login || null, targets: Array.isArray(b.targets) ? b.targets : [] });
|
|
138
|
+
signInStore.set({ structured: structured && Array.isArray(structured.selected) && Array.isArray(structured.pending) ? structured : null }); return b;
|
|
139
|
+
})
|
|
131
140
|
.catch(() => null)
|
|
132
141
|
.finally(() => { reading = null; if (again) { again = false; void refreshSignIn(); } });
|
|
133
142
|
return reading;
|
|
@@ -140,13 +149,25 @@ export function installSignIn() {
|
|
|
140
149
|
installed = true;
|
|
141
150
|
const on = appStore.get().streamOn;
|
|
142
151
|
if (typeof on === "function") on("backend_login", () => { void refreshSignIn(); });
|
|
143
|
-
void refreshSignIn()
|
|
152
|
+
void refreshSignIn().then(() => {
|
|
153
|
+
// A navigation hint from the installer, never authority to start a CLI or cancel another writer.
|
|
154
|
+
const backends = installerSignInBackends(typeof location === "undefined" ? "" : location.search);
|
|
155
|
+
if (backends && signInStore.get().structured) showDialog(BrowserSignInDialog, { backends });
|
|
156
|
+
});
|
|
144
157
|
shellStore.set({ banner: SignInLine });
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
export function installerSignInBackends(search) {
|
|
161
|
+
const csv = new URLSearchParams(search).get("backend_signins");
|
|
162
|
+
if (!csv || csv.length > 100) return null;
|
|
163
|
+
const ids = csv.split(",");
|
|
164
|
+
return ids.length <= 4 && ids.every(b => ["kiro-cli", "claude-code", "codex", "antigravity"].includes(b)) ? [...new Set(ids)] : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
/** Open the dialog for one login (a backend's default one, or a credential profile). */
|
|
148
168
|
export function openSignIn({ backend, profile }) {
|
|
149
|
-
|
|
169
|
+
if (!profile && ["kiro-cli", "claude-code", "codex"].includes(backend)) showDialog(BrowserSignInDialog, { backend });
|
|
170
|
+
else showDialog(SignInDialog, { backend, ...(profile ? { profile } : {}) });
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
/** Open the dialog on the running sign-in (its starter's view, or the cancel-only one). */
|
|
@@ -191,6 +212,32 @@ function Running({ login }) {
|
|
|
191
212
|
</div>`;
|
|
192
213
|
}
|
|
193
214
|
|
|
215
|
+
/** The same closed controls as first setup. Profile-specific logins retain their existing flow. */
|
|
216
|
+
export function BrowserSignInDialog({ backend, backends, onClose }) {
|
|
217
|
+
const root = useRef(null), close = onClose || closeDialog;
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
let cards, last = "";
|
|
220
|
+
cards = mountStructuredSignIns(root.current, { endpoint: "/ui/browser-backend-signins", changed: next => {
|
|
221
|
+
if (backends) { if (backends.some(b => !next.selected.includes(b))) void cards.addBackends(backends); }
|
|
222
|
+
else if (!next.selected.includes(backend)) void cards.addBackend(backend);
|
|
223
|
+
const signature = JSON.stringify([next.login, next.pending]);
|
|
224
|
+
if (signature !== last) { last = signature; void refreshSignIn(); }
|
|
225
|
+
} });
|
|
226
|
+
return () => cards.dispose();
|
|
227
|
+
}, [backend, backends]);
|
|
228
|
+
const fallback = async (chosen = backend) => {
|
|
229
|
+
const result = await post("/ui/browser-backend-signins", { operation: "cancel" });
|
|
230
|
+
if (!result.ok) { toast(tn("failed")); return; }
|
|
231
|
+
showDialog(SignInDialog, { backend: chosen });
|
|
232
|
+
};
|
|
233
|
+
return html`<${Dialog} title=${backends ? tn("listTitle") : tn("title", backend)} onClose=${close}>
|
|
234
|
+
<div ref=${root}></div>
|
|
235
|
+
${backends ? backends.filter(b => b !== "antigravity").map(b => html`<button type="button" class="btn" onClick=${() => fallback(b)}>${b} · ${tn("consoleFallback")}</button>`) : null}
|
|
236
|
+
<div class="s-actions">${backend ? html`<button type="button" class="btn" onClick=${() => fallback(backend)}>${tn("consoleFallback")}</button>` : null}
|
|
237
|
+
<button type="button" class="btn" onClick=${close}>${tn("close")}</button></div>
|
|
238
|
+
</${Dialog}>`;
|
|
239
|
+
}
|
|
240
|
+
|
|
194
241
|
export function SignInDialog({ backend, profile, resume = false, onClose }) {
|
|
195
242
|
const s = useStore(signInStore);
|
|
196
243
|
const [step, setStep] = useState(resume ? { kind: "login" } : { kind: "checking" });
|
|
@@ -265,7 +312,12 @@ export function SignInDialog({ backend, profile, resume = false, onClose }) {
|
|
|
265
312
|
|
|
266
313
|
/** The line every admin page shows while a sign-in runs (the shell's banner slot). */
|
|
267
314
|
export function SignInLine() {
|
|
268
|
-
const { login } = useStore(signInStore);
|
|
315
|
+
const { login, structured } = useStore(signInStore);
|
|
316
|
+
const active = structured?.login;
|
|
317
|
+
if ((!login || login.phase !== "running") && active && active.phase !== "done" && !(active.phase === "failed" && active.reason)) {
|
|
318
|
+
return html`<div data-trust="status" class="conn op-line signin-line" role="status"><span>${tn("browserRunning", active.backend)}</span>
|
|
319
|
+
<button type="button" class="btn btn-sm" onClick=${() => openSignIn({ backend: active.backend })}>${tn("open")}</button></div>`;
|
|
320
|
+
}
|
|
269
321
|
if (!login || login.phase !== "running") return null;
|
|
270
322
|
return html`<div data-trust="status" class="conn op-line signin-line" role="status">
|
|
271
323
|
<span>${tn("running", shownOf(login.backend, login.profile), login.startedOn)}</span>
|
|
@@ -276,13 +328,14 @@ const STATE_KEY = { usable: "stUsable", not_signed_in: "stNotSignedIn", not_inst
|
|
|
276
328
|
|
|
277
329
|
/** Settings → Agents: each login on this machine, with Sign in where the dashboard can do it. */
|
|
278
330
|
export function SignInList() {
|
|
279
|
-
const { targets, loaded, login } = useStore(signInStore);
|
|
331
|
+
const { targets, loaded, login, structured } = useStore(signInStore);
|
|
280
332
|
// Not over the temporary public link: only the dashboard reached directly signs a backend in, and the link does not
|
|
281
333
|
// serve this read (it was a 404 in the console there, found by smoke-1939-authorize-defaults).
|
|
282
334
|
const offered = signInOffered();
|
|
283
335
|
useEffect(() => { if (offered) void refreshSignIn(); }, []);
|
|
284
336
|
if (!offered || !loaded || !targets.length) return null;
|
|
285
|
-
const
|
|
337
|
+
const active = structured?.login;
|
|
338
|
+
const running = (login && login.phase === "running") || (active && active.phase !== "done" && !(active.phase === "failed" && active.reason));
|
|
286
339
|
return html`<section class="s-group signin-list" aria-label=${tn("listTitle")}>
|
|
287
340
|
<div class="list-head"><span>${tn("listTitle")}</span></div>
|
|
288
341
|
<p class="sub">${tn("listHint")}</p>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the public link or a read-only session (the host refuses them too).
|
|
9
9
|
import { html, useMemo, useState } from "/assets/app-html.js";
|
|
10
10
|
import { t, register } from "/assets/app-i18n.js";
|
|
11
|
-
import {
|
|
11
|
+
import { SidePanel } from "/assets/side-panel.js";
|
|
12
12
|
import { toast } from "/assets/ui-toast.js";
|
|
13
13
|
import { nodeText } from "./fleet-org.js";
|
|
14
14
|
|
|
@@ -147,8 +147,11 @@ export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () =
|
|
|
147
147
|
// the rev under an older draft (a save would then overwrite what it never showed). After a 409 the person chooses
|
|
148
148
|
// (#2431): keep their edits (on the newer rev: Save then replaces what was saved meanwhile, knowingly) or load the
|
|
149
149
|
// newer list — never their draft thrown away unasked.
|
|
150
|
-
|
|
150
|
+
// `saved`: the list the draft is measured against (as read, or the newer one loaded) — the draft holds changes when
|
|
151
|
+
// it differs (#2585: leaving the page beside the docked editor then asks first).
|
|
152
|
+
const [base, setBase] = useState(() => ({ rev, saved: JSON.stringify(offices), draft: offices.map((o) => ({ ...o, members: [...o.members] })) }));
|
|
151
153
|
const draft = base.draft;
|
|
154
|
+
const dirty = JSON.stringify(draft) !== base.saved;
|
|
152
155
|
const setDraft = (next) => setBase((b) => ({ ...b, draft: typeof next === "function" ? next(b.draft) : next }));
|
|
153
156
|
const [open, setOpen] = useState(null); // the office whose members are shown
|
|
154
157
|
const [busy, setBusy] = useState(false);
|
|
@@ -157,8 +160,8 @@ export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () =
|
|
|
157
160
|
const copy = (list) => list.map((o) => ({ ...o, members: [...o.members] }));
|
|
158
161
|
// The choice takes the snapshot in force when it is made: the stream may have read a newer one since the 409 (#2449).
|
|
159
162
|
const inForce = () => current() || conflict;
|
|
160
|
-
const keepMine = () => { const now = inForce(); setBase((b) => ({ rev: now.rev, draft: b.draft })); setConflict(null); setMsg(te("keptMine")); };
|
|
161
|
-
const loadTheirs = () => { const now = inForce(); setBase({ rev: now.rev, draft: copy(now.offices) }); setOpen(null); setConflict(null); setMsg(""); };
|
|
163
|
+
const keepMine = () => { const now = inForce(); setBase((b) => ({ rev: now.rev, saved: JSON.stringify(now.offices), draft: b.draft })); setConflict(null); setMsg(te("keptMine")); };
|
|
164
|
+
const loadTheirs = () => { const now = inForce(); setBase({ rev: now.rev, saved: JSON.stringify(now.offices), draft: copy(now.offices) }); setOpen(null); setConflict(null); setMsg(""); };
|
|
162
165
|
const everyone = useMemo(() => { const m = new Map(); for (const n of [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes]) if (!m.has(n.name)) m.set(n.name, n); return [...m.values()]; }, [view]);
|
|
163
166
|
const set = (id, patch) => setDraft((d) => d.map((o) => (o.id === id ? { ...o, ...patch } : o)));
|
|
164
167
|
const add = () => {
|
|
@@ -191,7 +194,8 @@ export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () =
|
|
|
191
194
|
};
|
|
192
195
|
const actions = html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${te("cancel")}</button>
|
|
193
196
|
<button type="button" class="btn btn-primary" id="oeSave" onClick=${save} disabled=${busy || !!conflict}>${te("save")}</button>`;
|
|
194
|
-
|
|
197
|
+
// #2585: docked on the right on a wide desk (the page beside it in use); the centred dialog elsewhere.
|
|
198
|
+
return html`<${SidePanel} title=${te("title")} onClose=${onClose} actions=${actions} busy=${busy} dirty=${dirty}>
|
|
195
199
|
<p class="note">${te("lead")}</p>
|
|
196
200
|
<fieldset class="oe-form" disabled=${busy}>
|
|
197
201
|
${draft.length ? null : html`<p class="note" id="oeNone">${te("none")}</p>`}
|
|
@@ -220,5 +224,5 @@ export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () =
|
|
|
220
224
|
<button type="button" class="btn btn-sm" id="oeKeepMine" onClick=${keepMine} disabled=${busy}>${te("keepMine")}</button>
|
|
221
225
|
<button type="button" class="btn btn-sm" id="oeLoadTheirs" onClick=${loadTheirs} disabled=${busy}>${te("loadTheirs")}</button>
|
|
222
226
|
</div>` : null}
|
|
223
|
-
</${
|
|
227
|
+
</${SidePanel}>`;
|
|
224
228
|
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// the public link or a read-only session (the host refuses their saves too).
|
|
10
10
|
//
|
|
11
11
|
// The pure part (handles, moving, adding and removing) is exported for tests; LayoutEditor is the overlay.
|
|
12
|
-
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
12
|
+
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
|
|
13
13
|
import { t, register } from "/assets/app-i18n.js";
|
|
14
|
+
import { SidePanel } from "/assets/side-panel.js";
|
|
14
15
|
import { LOGICAL, overridesBetween, own } from "./fleet-office-pack.js";
|
|
15
16
|
|
|
16
17
|
register("orgl", {
|
|
@@ -96,7 +97,7 @@ const KEY_STEP = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], Arr
|
|
|
96
97
|
* moves (the canvas redraws it); `onSave(overrides | null)` → { ok } or { ok: false, message }; `onClose()`. `stale` (a message): the
|
|
97
98
|
* office or its style changed underneath the session — no handles, nothing to save, only Cancel.
|
|
98
99
|
*/
|
|
99
|
-
export function LayoutEditor({ base, start, place = null, stale = "", onChange, onSave, onClose }) {
|
|
100
|
+
export function LayoutEditor({ base, start, place = null, stale = "", dock = true, onChange, onSave, onClose }) {
|
|
100
101
|
const [edited, setEdited] = useState(start);
|
|
101
102
|
const [sel, setSel] = useState(null);
|
|
102
103
|
const [coords, setCoords] = useState(true);
|
|
@@ -149,13 +150,38 @@ export function LayoutEditor({ base, start, place = null, stale = "", onChange,
|
|
|
149
150
|
}
|
|
150
151
|
const at = new Map(edited.waypoints.map((w) => [w.id, w]));
|
|
151
152
|
const edgeKey = (e) => `${e[0]}\u0000${e[1]}`;
|
|
153
|
+
// #2585: the controls dock in the side panel on a wide desk (the canvas left whole for the handles), else they are
|
|
154
|
+
// the bar over the canvas as before. Esc (in the panel, or on the canvas's handles) is Cancel, never mid-save.
|
|
155
|
+
const esc = (e) => { if (e.key === "Escape" && !e.defaultPrevented && !busy) { e.preventDefault(); onClose(); } };
|
|
156
|
+
const dirty = useMemo(() => JSON.stringify(edited) !== JSON.stringify(start), [edited, start]);
|
|
157
|
+
const panel = (controls, actions, tail) => html`<${SidePanel} title=${tl("title")} dock=${dock} busy=${busy} dirty=${dirty} onClose=${onClose} actions=${actions}
|
|
158
|
+
fallback=${html`<div class="ol-bar">${controls}<span class="ol-actions">${actions}</span>${tail}</div>`}>
|
|
159
|
+
<div class="ol-side">${controls}${tail}</div>
|
|
160
|
+
</${SidePanel}>`;
|
|
152
161
|
if (stale) {
|
|
153
|
-
return html`<div class="ol" role="group" aria-label=${tl("title")}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</div
|
|
162
|
+
return html`<div class="ol" role="group" aria-label=${tl("title")} onKeyDown=${esc}>
|
|
163
|
+
${panel(null, html`<button type="button" class="btn btn-sm ol-cancel" onClick=${onClose} disabled=${busy}>${tl("cancel")}</button>`,
|
|
164
|
+
html`<p class="note ol-msg ol-stale" role="status">${stale}</p>`)}
|
|
165
|
+
</div>`;
|
|
157
166
|
}
|
|
158
|
-
|
|
167
|
+
const controls = html`<label class="check"><input type="checkbox" class="ol-coords" checked=${coords} onChange=${() => setCoords((v) => !v)} /><span>${tl("coords")}</span></label>
|
|
168
|
+
<button type="button" class="btn btn-sm ol-add" onClick=${add} disabled=${busy}>${tl("addWaypoint")}</button>
|
|
169
|
+
<span class="muted ol-sel" aria-live="polite">${selected ? tl("at", selected.label, selected.x, selected.y) : tl("none")}</span>
|
|
170
|
+
${wpSel ? html`<button type="button" class="btn btn-sm btn-danger ol-remove" onClick=${() => { change(removeWaypoint(edited, wpSel)); setSel(null); }} disabled=${busy}>${tl("removeWaypoint")}</button>
|
|
171
|
+
<label class="oo-pick"><span class="oo-pick-label">${tl("joinTo")}</span>
|
|
172
|
+
<select class="tb-select ol-join-to" value=${joinTo} onChange=${(e) => setJoinTo(e.target.value)}>
|
|
173
|
+
<option value="">—</option>
|
|
174
|
+
${edited.waypoints.filter((w) => w.id !== wpSel).map((w) => html`<option key=${w.id} value=${w.id}>${w.id}</option>`)}
|
|
175
|
+
</select></label>
|
|
176
|
+
<button type="button" class="btn btn-sm ol-join" disabled=${busy || !joinTo || joinTo === wpSel} onClick=${() => { change(toggleEdge(edited, wpSel, joinTo)); setJoinTo(""); }}>${tl("join")}</button>
|
|
177
|
+
<span class="ol-corridors">${edited.edges.filter((e) => e[0] === wpSel || e[1] === wpSel).map((e) => { const other = e[0] === wpSel ? e[1] : e[0];
|
|
178
|
+
return html`<button key=${edgeKey(e)} type="button" class="btn btn-sm ol-unjoin" data-other=${other} disabled=${busy} onClick=${() => change(toggleEdge(edited, wpSel, other))}>${tl("unjoin", other)}</button>`; })}</span>` : null}`;
|
|
179
|
+
const actions = html`<button type="button" class="btn btn-sm ol-reset" onClick=${reset} disabled=${busy}>${tl("reset")}</button>
|
|
180
|
+
<button type="button" class="btn btn-sm ol-cancel" onClick=${onClose} disabled=${busy}>${tl("cancel")}</button>
|
|
181
|
+
<button type="button" class="btn btn-sm btn-primary ol-save" onClick=${save} disabled=${busy}>${tl("save")}</button>`;
|
|
182
|
+
const tail = html`<span class="muted oo-note">${tl("hint")}</span>
|
|
183
|
+
${msg ? html`<p class="note ol-msg" role="status">${msg}</p>` : null}`;
|
|
184
|
+
return html`<div class="ol" role="group" aria-label=${tl("title")} onKeyDown=${esc}>
|
|
159
185
|
<div class="ol-overlay" ref=${overlay}>
|
|
160
186
|
<svg ref=${svg} class="ol-svg" viewBox=${`0 0 ${LOGICAL.w} ${LOGICAL.h}`} width="100%" height="100%"
|
|
161
187
|
onPointerMove=${move} onPointerUp=${up} onPointerCancel=${up} onPointerLeave=${up}>
|
|
@@ -172,26 +198,6 @@ export function LayoutEditor({ base, start, place = null, stale = "", onChange,
|
|
|
172
198
|
</g>`)}
|
|
173
199
|
</svg>
|
|
174
200
|
</div>
|
|
175
|
-
|
|
176
|
-
<label class="check"><input type="checkbox" class="ol-coords" checked=${coords} onChange=${() => setCoords((v) => !v)} /><span>${tl("coords")}</span></label>
|
|
177
|
-
<button type="button" class="btn btn-sm ol-add" onClick=${add} disabled=${busy}>${tl("addWaypoint")}</button>
|
|
178
|
-
<span class="muted ol-sel" aria-live="polite">${selected ? tl("at", selected.label, selected.x, selected.y) : tl("none")}</span>
|
|
179
|
-
${wpSel ? html`<button type="button" class="btn btn-sm btn-danger ol-remove" onClick=${() => { change(removeWaypoint(edited, wpSel)); setSel(null); }} disabled=${busy}>${tl("removeWaypoint")}</button>
|
|
180
|
-
<label class="oo-pick"><span class="oo-pick-label">${tl("joinTo")}</span>
|
|
181
|
-
<select class="tb-select ol-join-to" value=${joinTo} onChange=${(e) => setJoinTo(e.target.value)}>
|
|
182
|
-
<option value="">—</option>
|
|
183
|
-
${edited.waypoints.filter((w) => w.id !== wpSel).map((w) => html`<option key=${w.id} value=${w.id}>${w.id}</option>`)}
|
|
184
|
-
</select></label>
|
|
185
|
-
<button type="button" class="btn btn-sm ol-join" disabled=${busy || !joinTo || joinTo === wpSel} onClick=${() => { change(toggleEdge(edited, wpSel, joinTo)); setJoinTo(""); }}>${tl("join")}</button>
|
|
186
|
-
<span class="ol-corridors">${edited.edges.filter((e) => e[0] === wpSel || e[1] === wpSel).map((e) => { const other = e[0] === wpSel ? e[1] : e[0];
|
|
187
|
-
return html`<button key=${edgeKey(e)} type="button" class="btn btn-sm ol-unjoin" data-other=${other} disabled=${busy} onClick=${() => change(toggleEdge(edited, wpSel, other))}>${tl("unjoin", other)}</button>`; })}</span>` : null}
|
|
188
|
-
<span class="ol-actions">
|
|
189
|
-
<button type="button" class="btn btn-sm ol-reset" onClick=${reset} disabled=${busy}>${tl("reset")}</button>
|
|
190
|
-
<button type="button" class="btn btn-sm ol-cancel" onClick=${onClose} disabled=${busy}>${tl("cancel")}</button>
|
|
191
|
-
<button type="button" class="btn btn-sm btn-primary ol-save" onClick=${save} disabled=${busy}>${tl("save")}</button>
|
|
192
|
-
</span>
|
|
193
|
-
<span class="muted oo-note">${tl("hint")}</span>
|
|
194
|
-
${msg ? html`<p class="note ol-msg" role="status">${msg}</p>` : null}
|
|
195
|
-
</div>
|
|
201
|
+
${panel(controls, actions, tail)}
|
|
196
202
|
</div>`;
|
|
197
203
|
}
|
|
@@ -199,7 +199,9 @@ export function autoLayout(rooms, { width = 960 } = {}) {
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
const right = out.length ? Math.max(...out.map((r) => r.x + r.w)) : 0, bottom = out.length ? Math.max(...out.map((r) => r.y + r.h)) : 0;
|
|
202
|
-
|
|
202
|
+
// As wide as its rooms (rows wrap at `width`): a scene narrower than the stage is centred in it (clampPan), never
|
|
203
|
+
// left at the stage's left edge with the rest of the stage empty (#2585 B).
|
|
204
|
+
return { rooms: out, waypoints, edges, width: out.length ? right + M : width, height: bottom + GAP / 2 + M };
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
/**
|
|
@@ -318,7 +320,8 @@ const MAX_SEEN = 2000;
|
|
|
318
320
|
* The items in the air. `sync(items, now, still)` takes fleet-traffic.js's store items: each new one (a message, a
|
|
319
321
|
* delegation's work, its report coming back) starts one trip from its sender's desk to its receiver's; a delegation with
|
|
320
322
|
* no report (stale) and anything seen before start none; `still` (reduced motion) starts none. `frame(now, L)` the
|
|
321
|
-
* trips under way on layout L: { id, hue, kind, x, y, t }
|
|
323
|
+
* trips under way on layout L: { id, hue, kind, from, x, y, t } (from: who sent it — a pack can draw it as theirs),
|
|
324
|
+
* finished ones dropped; a trip whose desks are not drawn goes.
|
|
322
325
|
*/
|
|
323
326
|
export function createFlights({ max = FLIGHT.MAX } = {}) {
|
|
324
327
|
const seen = new Set();
|
|
@@ -344,7 +347,7 @@ export function createFlights({ max = FLIGHT.MAX } = {}) {
|
|
|
344
347
|
if (!f.pts) return false;
|
|
345
348
|
const t = (now - f.start) / f.ms;
|
|
346
349
|
if (t >= 1) return false;
|
|
347
|
-
if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, x: p.x, y: p.y, t }); }
|
|
350
|
+
if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, from: f.from, x: p.x, y: p.y, t }); }
|
|
348
351
|
return true;
|
|
349
352
|
});
|
|
350
353
|
return out;
|
|
@@ -130,7 +130,7 @@ export function crossPath(parts, from, to) {
|
|
|
130
130
|
const MAX_SEEN = 2000;
|
|
131
131
|
/**
|
|
132
132
|
* The items in the air between tiles — fleet-office-model.js createFlights's lifecycle (each new item once; none for a
|
|
133
|
-
* stale delegation or with reduced motion), on crossPath's straight line: `frame(now, parts)` → { id, hue, kind, x, y,
|
|
133
|
+
* stale delegation or with reduced motion), on crossPath's straight line: `frame(now, parts)` → { id, hue, kind, from, x, y,
|
|
134
134
|
* t } in overview units; a trip with no crossing (both in one tile, or either gone) is dropped.
|
|
135
135
|
*/
|
|
136
136
|
export function createCrossFlights({ max = FLIGHT.MAX } = {}) {
|
|
@@ -157,7 +157,7 @@ export function createCrossFlights({ max = FLIGHT.MAX } = {}) {
|
|
|
157
157
|
if (!f.pts) return false;
|
|
158
158
|
const t = (now - f.start) / f.ms;
|
|
159
159
|
if (t >= 1) return false;
|
|
160
|
-
if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, x: p.x, y: p.y, t }); }
|
|
160
|
+
if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, from: f.from, x: p.x, y: p.y, t }); }
|
|
161
161
|
return true;
|
|
162
162
|
});
|
|
163
163
|
return out;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
export interface PackStrip { src: string; frames: number; w: number; h: number; fps: number; ax: number; ay: number; dw: number; dh: number }
|
|
5
5
|
export interface PackPoint { x: number; y: number }
|
|
6
6
|
export interface PackArea { name: string; rect: { x: number; y: number; w: number; h: number }; seats: PackPoint[] }
|
|
7
|
-
|
|
7
|
+
/** `label` (#2574): its name tag, or null. */
|
|
8
|
+
export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null; label: string | null }
|
|
8
9
|
export interface CheckedPack {
|
|
9
10
|
format: 1; id: string; scale: number; background?: string; foreground?: string; matte?: string;
|
|
10
11
|
areas: Record<string, PackArea>; stateZones: Record<string, string> | null; door: PackPoint;
|
|
@@ -36,4 +37,5 @@ export function checkLayoutOverrides(x: unknown): { ok: true; overrides: LayoutO
|
|
|
36
37
|
export function applyOverrides(pack: CheckedPack, ov: LayoutOverrides | null | undefined): { ok: true; pack: CheckedPack } | { ok: false; error: string };
|
|
37
38
|
export function overridesBetween(pack: CheckedPack, edited: CheckedPack): LayoutOverrides | null;
|
|
38
39
|
export function poseStrip(set: Record<string, PackStrip> | null | undefined, pose: string, heading?: { dx: number; dy: number } | null): { strip: PackStrip; mirror: boolean } | null;
|
|
40
|
+
export function walkStrip(f: { walk?: Record<string, PackStrip> | null; walking?: boolean; heading?: { dx: number; dy: number } | null } | null | undefined): { strip: PackStrip; mirror: boolean } | null;
|
|
39
41
|
export function stripBox(st: PackStrip, x: number, y: number, mirror?: boolean): { x: number; y: number; w: number; h: number };
|
|
@@ -31,14 +31,27 @@
|
|
|
31
31
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
|
|
32
32
|
// optional: the walkable graph (else people walk straight)
|
|
33
33
|
// "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
|
|
34
|
-
// "activeWhen": "crashed", "activeSprite": STRIP
|
|
34
|
+
// "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
|
|
35
|
+
// "night": { "sprite": STRIP, "x": 300, "y": 420 },
|
|
36
|
+
// "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
|
|
37
|
+
// "label" (#2574): a name tag under it, like a person's
|
|
38
|
+
// (an NPC drawn as furniture). "night" (#2568), optional: at bedtime
|
|
39
|
+
// (when the viewer has it on: 00:00–06:00 their time) drawn as this
|
|
40
|
+
// strip, playing, at (x, y) — its own place for either left out.
|
|
41
|
+
// "walk" (#2596), optional: it strolls near its (x, y) and back
|
|
42
|
+
// like an idle person, drawn walking by these strips as a
|
|
43
|
+
// character set's walking poses ("walk" required); not at bedtime
|
|
44
|
+
// with a "night".
|
|
35
45
|
// drawn by depth: below 1000 under the people, 1000 and up over
|
|
36
46
|
// them; "depth": "y" (the string) sorts it among the people by base
|
|
37
47
|
// y — its anchor's y against each person's feet: someone whose feet
|
|
38
48
|
// are lower down (a greater y; a tie too) is drawn after it, in
|
|
39
49
|
// front. (x, y) is where the strip's anchor goes. `activeWhen` (a
|
|
40
50
|
// state, or a list): while anyone shown is in it, the item plays its
|
|
41
|
-
// strip (or `activeSprite`); otherwise it holds the first frame
|
|
51
|
+
// strip (or `activeSprite`); otherwise it holds the first frame —
|
|
52
|
+
// or, with "loop": true (#2570), plays its strip all along.
|
|
53
|
+
// "activeFor" (an instance's name, or a list): only their state
|
|
54
|
+
// counts, not anyone's (a room several people take turns in)
|
|
42
55
|
// "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
|
|
43
56
|
// "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
|
|
44
57
|
// "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
|
|
@@ -47,6 +60,9 @@
|
|
|
47
60
|
// walkSide, drawn as it is when moving right and mirrored when moving
|
|
48
61
|
// left (so draw it facing right). A missing one is "walk", then idle
|
|
49
62
|
// ("walk" itself is never mirrored)
|
|
63
|
+
// A set may also have "prop": STRIP (#2562) — what its people
|
|
64
|
+
// send: their messages, work and reports fly as it, in place of
|
|
65
|
+
// the pack's "props" (a set without one: the pack's props)
|
|
50
66
|
// "byInstance": { "atlas": "fox" }, optional: an instance name → its character set
|
|
51
67
|
// "props": { "task": STRIP, "send": STRIP, "report": STRIP, "broadcast": STRIP }, optional traffic items
|
|
52
68
|
// "bubbles": { "en": { "working": ["On it."] }, "zh-TW": { … } } optional: short lines per state (≤ 12 a state,
|
|
@@ -96,6 +112,10 @@ export const LOGICAL = { w: 1280, h: 720 };
|
|
|
96
112
|
export const CHAR_DEPTH = 1000;
|
|
97
113
|
export const PACK_STATES = ["needs", "crashed", "stuck", "working", "idle", "paused", "stopped", "missing"];
|
|
98
114
|
const POSES = ["working", "idle", "paused", "needs", "walk", "walkDown", "walkUp", "walkSide"], PROPS = ["task", "send", "report", "broadcast"];
|
|
115
|
+
/** A character set's strips: its poses, and what its people send (#2562). */
|
|
116
|
+
const SET_STRIPS = [...POSES, "prop"];
|
|
117
|
+
/** A furniture item's walking strips (#2596): a character set's walking poses. */
|
|
118
|
+
const WALKS = ["walk", "walkDown", "walkUp", "walkSide"];
|
|
99
119
|
const BUILT_IN = new Set(["pixel", "studio"]);
|
|
100
120
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
101
121
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
@@ -138,6 +158,24 @@ export function checkStrip(v, at = "strip", scale = 2) {
|
|
|
138
158
|
return { src: src(v.src, `${at}.src`), frames: int(v.frames, 1, 32, `${at}.frames`), w, h, fps: v.fps === undefined ? 4 : num(v.fps, 0, 30, `${at}.fps`),
|
|
139
159
|
ax: v.ax === undefined ? w / 2 : num(v.ax, 0, w, `${at}.ax`), ay: v.ay === undefined ? h : num(v.ay, 0, h, `${at}.ay`), dw, dh };
|
|
140
160
|
}
|
|
161
|
+
/** A furniture item's night (#2568): { sprite, x, y } — at bedtime it is drawn as `sprite` (playing) at (x, y), its own place when left out. */
|
|
162
|
+
function nightOf(v, at, scale) {
|
|
163
|
+
if (v === undefined) return null;
|
|
164
|
+
obj(v, at); keys(v, ["sprite", "x", "y"], at);
|
|
165
|
+
return { sprite: checkStrip(v.sprite, `${at}.sprite`, scale), x: v.x === undefined ? null : num(v.x, 0, LOGICAL.w, `${at}.x`), y: v.y === undefined ? null : num(v.y, 0, LOGICAL.h, `${at}.y`) };
|
|
166
|
+
}
|
|
167
|
+
/** A furniture item's walk (#2596): its walking strips, picked by heading as a character set's (poseStrip); "walk" required. */
|
|
168
|
+
function walkOf(v, at, scale) {
|
|
169
|
+
if (v === undefined) return null;
|
|
170
|
+
const w = strips(v, WALKS, at, scale);
|
|
171
|
+
if (!w.walk) throw new Refused(`${at}.walk`, "required");
|
|
172
|
+
return w;
|
|
173
|
+
}
|
|
174
|
+
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to 8. */
|
|
175
|
+
function names(v, at) {
|
|
176
|
+
const one = (s, here) => { if (typeof s !== "string" || !s || s.length > 200 || inherited(s) || /[\u0000-\u001f\u007f]/.test(s)) throw new Refused(here, "an instance's name"); return s; };
|
|
177
|
+
return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
178
|
+
}
|
|
141
179
|
function strips(v, names, at, scale) {
|
|
142
180
|
obj(v, at);
|
|
143
181
|
keys(v, names, at);
|
|
@@ -204,20 +242,25 @@ export function checkSkinPack(x) {
|
|
|
204
242
|
const fids = new Set();
|
|
205
243
|
const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
|
|
206
244
|
const at = `furniture[${i}]`;
|
|
207
|
-
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite"], at);
|
|
245
|
+
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
|
|
208
246
|
if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
|
|
209
247
|
fids.add(f.id);
|
|
210
248
|
if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
|
|
249
|
+
if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
|
|
250
|
+
if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
|
|
251
|
+
if (f.label !== undefined && (typeof f.label !== "string" || !f.label.trim() || [...f.label].length > LIMIT.line || /[\u0000-\u001f\u007f]/.test(f.label))) throw new Refused(`${at}.label`, `1–${LIMIT.line} characters`);
|
|
211
252
|
return { id: f.id, sprite: checkStrip(f.sprite, `${at}.sprite`, scale), x: num(f.x, 0, LOGICAL.w, `${at}.x`), y: num(f.y, 0, LOGICAL.h, `${at}.y`),
|
|
212
253
|
depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
|
|
213
|
-
activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale)
|
|
254
|
+
activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
|
|
255
|
+
activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
|
|
256
|
+
walk: walkOf(f.walk, `${at}.walk`, scale) };
|
|
214
257
|
}).sort((a, b) => order(a) - order(b));
|
|
215
258
|
const sets = obj(x.characters, "characters");
|
|
216
259
|
if (Object.keys(sets).length > LIMIT.sets) throw new Refused("characters", `at most ${LIMIT.sets} sets`);
|
|
217
260
|
const characters = {};
|
|
218
261
|
for (const [k, v] of Object.entries(sets)) {
|
|
219
262
|
if (!nameOk(k)) throw new Refused(`characters.${k}`, "a name of letters, digits, _ or - (not an inherited one)");
|
|
220
|
-
characters[k] = strips(v,
|
|
263
|
+
characters[k] = strips(v, SET_STRIPS, `characters.${k}`, scale);
|
|
221
264
|
if (!characters[k].idle) throw new Refused(`characters.${k}.idle`, "required");
|
|
222
265
|
}
|
|
223
266
|
if (!own(characters, "default")) throw new Refused("characters.default", "required");
|
|
@@ -261,13 +304,18 @@ export function checkSkinPack(x) {
|
|
|
261
304
|
export const skinPackOk = (x) => { const r = checkSkinPack(x); return r.ok ? r.pack : null; };
|
|
262
305
|
/** Every image a pack names (what a loader fetches before spriteSkin draws). */
|
|
263
306
|
export const packImages = (pack) => [...new Set([pack.background, pack.foreground, pack.room.floor, pack.room.wall, pack.desk.back, pack.desk.front, pack.desk.lit,
|
|
264
|
-
...pack.furniture.flatMap((f) => [f.sprite.src, f.activeSprite && f.activeSprite.src]),
|
|
307
|
+
...pack.furniture.flatMap((f) => [f.sprite.src, f.activeSprite && f.activeSprite.src, f.night && f.night.sprite.src, ...Object.values(f.walk || {}).map((st) => st.src)]),
|
|
265
308
|
...Object.values(pack.characters).flatMap((s) => Object.values(s).map((st) => st.src)), ...Object.values(pack.props).map((st) => st.src)].filter(Boolean))];
|
|
266
309
|
|
|
267
310
|
/** Every frame strip a checked pack names, with where (the host checks each image is frames·w × h). */
|
|
268
311
|
export function packStrips(pack) {
|
|
269
312
|
const out = [];
|
|
270
|
-
for (const f of pack.furniture) {
|
|
313
|
+
for (const f of pack.furniture) {
|
|
314
|
+
out.push({ at: `furniture.${f.id}.sprite`, strip: f.sprite });
|
|
315
|
+
if (f.activeSprite) out.push({ at: `furniture.${f.id}.activeSprite`, strip: f.activeSprite });
|
|
316
|
+
if (f.night) out.push({ at: `furniture.${f.id}.night.sprite`, strip: f.night.sprite });
|
|
317
|
+
for (const [k, st] of Object.entries(f.walk || {})) out.push({ at: `furniture.${f.id}.walk.${k}`, strip: st });
|
|
318
|
+
}
|
|
271
319
|
for (const [k, set] of Object.entries(pack.characters)) for (const [pose, st] of Object.entries(set)) out.push({ at: `characters.${k}.${pose}`, strip: st });
|
|
272
320
|
for (const [k, st] of Object.entries(pack.props)) out.push({ at: `props.${k}`, strip: st });
|
|
273
321
|
return out;
|
|
@@ -403,6 +451,9 @@ export function poseStrip(set, pose, heading = null) {
|
|
|
403
451
|
const st = own(set, pose) || own(set, "idle");
|
|
404
452
|
return st ? { strip: st, mirror: false } : null;
|
|
405
453
|
}
|
|
454
|
+
/** The strip a furniture item shows while it walks (#2596: an NPC with `walk`, `walking` and `heading` set where it is
|
|
455
|
+
* drawn): its walk strip by heading, as a character's (poseStrip); null when it stands (its own sprite, as before). */
|
|
456
|
+
export const walkStrip = (f) => (f && f.walking && f.walk ? poseStrip(f.walk, "walk", f.heading) : null);
|
|
406
457
|
/** The box (logical units) a strip covers with its anchor at (x, y): its displayed size; mirrored about the anchor. */
|
|
407
458
|
export function stripBox(st, x, y, mirror = false) {
|
|
408
459
|
const kx = st.dw / st.w, ky = st.dh / st.h;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people (depth "y":
|
|
42
42
|
// among them by base y), then the foreground, then the crowd counts, tags and bubbles.
|
|
43
43
|
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
44
|
-
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox } from "./fleet-office-pack.js";
|
|
44
|
+
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
|
|
45
45
|
|
|
46
46
|
export { nameHash };
|
|
47
47
|
|
|
@@ -513,8 +513,12 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
513
513
|
},
|
|
514
514
|
drawFurniture(ctx, f, active, ms, env) {
|
|
515
515
|
if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
|
|
516
|
-
//
|
|
516
|
+
// An NPC walking (#2596): its walk strip for its heading (walkSide mirrored going left), when its picture is loaded.
|
|
517
|
+
const ws = walkStrip(f);
|
|
518
|
+
if (ws && strip(ctx, ws.strip, ms, f.x, f.y, null, ws.mirror)) return;
|
|
519
|
+
// Active: its activeSprite (or its strip) plays; otherwise its strip loops ("loop", #2570) or its first frame holds.
|
|
517
520
|
if (active) strip(ctx, f.activeSprite || f.sprite, ms, f.x, f.y);
|
|
521
|
+
else if (f.loop) strip(ctx, f.sprite, ms, f.x, f.y);
|
|
518
522
|
else strip(ctx, f.sprite, 0, f.x, f.y, 0);
|
|
519
523
|
},
|
|
520
524
|
drawCharacter(ctx, d, look, ms, env) {
|
|
@@ -530,7 +534,11 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
530
534
|
const ps = shown({ ...look, heading: at.heading }, !!at.walking);
|
|
531
535
|
return ps ? stripBox(ps.strip, at.x, at.y, ps.mirror) : base.charBox(look, at);
|
|
532
536
|
},
|
|
533
|
-
|
|
537
|
+
// An item in the air: its sender's own (their set's "prop", #2562), else the pack's for its kind, else the built-in one.
|
|
538
|
+
drawProp(ctx, it, ms, env) {
|
|
539
|
+
const mine = it.from ? own(setOf({ name: it.from }) || {}, "prop") : null;
|
|
540
|
+
if (!strip(ctx, mine || own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env);
|
|
541
|
+
},
|
|
534
542
|
};
|
|
535
543
|
}
|
|
536
544
|
|
|
@@ -541,7 +549,8 @@ export const BUILT_IN_SKINS = ["pixel", "studio"];
|
|
|
541
549
|
export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
|
|
542
550
|
|
|
543
551
|
const loaded = new Map(); // `${id}@${sha}` → Promise<OfficeSkin | null>
|
|
544
|
-
|
|
552
|
+
// Room for every pack the host keeps (web-office-skins.ts SKIN_LIMITS.packs) and a few replaced ones (each a new sha).
|
|
553
|
+
const MAX_LOADED = 24;
|
|
545
554
|
const defaultImage = (url) => new Promise((resolve) => {
|
|
546
555
|
if (typeof Image !== "function") { resolve(null); return; }
|
|
547
556
|
const im = new Image();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
11
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
12
12
|
|
|
13
|
-
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep";
|
|
13
|
+
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep", NIGHT_KEY = "agend_org_office_night";
|
|
14
14
|
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
15
15
|
const LOBBY_KEY = "agend_org_office_lobby";
|
|
16
16
|
/** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
|
|
@@ -30,6 +30,20 @@ export function skinListOk(d) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
|
|
33
|
+
/**
|
|
34
|
+
* Agents' acts as the host sends them (GET /ui/office-acts, the `office_acts` event), checked: the well-formed ones.
|
|
35
|
+
* `now`: the host's clock when it sent them — each end is moved onto this page's clock (they may differ).
|
|
36
|
+
*/
|
|
37
|
+
export function actsOk(list, now) {
|
|
38
|
+
const skew = typeof now === "number" && Number.isFinite(now) ? now - Date.now() : 0;
|
|
39
|
+
const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 200, num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
40
|
+
return (Array.isArray(list) ? list : []).filter((a) => a && typeof a === "object" && str(a.instance) && str(a.office) && num(a.until)
|
|
41
|
+
&& (a.x === undefined || num(a.x)) && (a.y === undefined || num(a.y)) && (a.person === undefined || str(a.person))
|
|
42
|
+
&& (a.say === undefined || (typeof a.say === "string" && a.say.length <= 40)) && (a.pose === undefined || ["idle", "working", "needs", "paused"].includes(a.pose)))
|
|
43
|
+
.map((a) => ({ instance: a.instance, office: a.office, until: a.until - skew, ...(num(a.since) ? { since: a.since } : {}), ...(str(a.state) ? { state: a.state } : {}),
|
|
44
|
+
...(num(a.x) && num(a.y) ? { x: a.x, y: a.y } : {}),
|
|
45
|
+
...(a.person ? { person: a.person } : {}), ...(a.say ? { say: a.say } : {}), ...(a.pose ? { pose: a.pose } : {}) }));
|
|
46
|
+
}
|
|
33
47
|
export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
|
|
34
48
|
const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
|
|
35
49
|
const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
|
|
@@ -57,6 +71,15 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
57
71
|
return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
|
|
58
72
|
} catch { return { ok: false, status: 0, error: "offices_failed" }; }
|
|
59
73
|
},
|
|
74
|
+
/** #2578: what agents are doing in the offices now (office_set): their acts, or null when they could not be read. */
|
|
75
|
+
async acts() {
|
|
76
|
+
try {
|
|
77
|
+
const r = await get("/ui/office-acts", { method: "GET", headers });
|
|
78
|
+
if (!r || !r.ok) return null;
|
|
79
|
+
const d = await r.json();
|
|
80
|
+
return actsOk(d && d.acts, d && d.now);
|
|
81
|
+
} catch { return null; }
|
|
82
|
+
},
|
|
60
83
|
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
61
84
|
async skins() {
|
|
62
85
|
try {
|
|
@@ -96,6 +119,9 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
96
119
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
97
120
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
98
121
|
saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
|
|
122
|
+
/** #2568: whether the idle go to bed at night (00:00–06:00 on this device's clock) here (default: off). */
|
|
123
|
+
nightOn: () => read(NIGHT_KEY) === "1",
|
|
124
|
+
saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
|
|
99
125
|
/** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
|
|
100
126
|
lobbyTile: () => read(LOBBY_KEY) !== "0",
|
|
101
127
|
saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
|