@songsid/agend 2.3.4-beta.2 → 2.3.4-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-api.js +3 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +1 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +116 -20
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +2 -0
- package/dist/backend/grok.js +22 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +2 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +3 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +10 -1
- package/dist/daemon.js +196 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.test.js +2 -2
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- package/dist/fleet-manager.d.ts +4 -0
- package/dist/fleet-manager.js +46 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -2
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +12 -0
- package/dist/instance-lifecycle.js +31 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +26 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +2 -0
- package/dist/log-service.js +7 -3
- package/dist/log-service.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/outbound-handlers.js +56 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +4 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/selection-card.d.ts +2 -3
- package/dist/selection-card.js +99 -26
- package/dist/selection-card.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/ui/chat-store.js +20 -3
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-shell.js +4 -2
- package/dist/ui/shared/app-stream.js +26 -0
- package/dist/ui/shared/app.css +21 -1
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +185 -10
- package/dist/ui/shared/view-strings.js +23 -0
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +65 -0
- package/dist/unattended-install.js.map +1 -0
- package/dist/view-api.d.ts +15 -0
- package/dist/view-api.js +94 -33
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.js +7 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/chat-store.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// navigation — it counts as use, as the old click did). Coming back to it reads nothing; the stream keeps it current.
|
|
10
10
|
import "./chat-render.js";
|
|
11
11
|
import { createLiveAgentReplies } from "./chat-live-replies.js";
|
|
12
|
+
/** A message's identity on this page (ids restart with each fleet process; the boot makes them unique). */
|
|
13
|
+
const msgKeyOf = (m) => `${m.boot}-${m.id}`;
|
|
12
14
|
|
|
13
15
|
const R = () => globalThis.AgendChatRender;
|
|
14
16
|
|
|
@@ -43,7 +45,7 @@ const COMPACT_PAGE_LIMIT_MS = 11 * 60_000;
|
|
|
43
45
|
export function createChatStore(deps) {
|
|
44
46
|
const s = {
|
|
45
47
|
msgs: {}, drafts: {}, replies: {}, pendingFiles: {}, sending: {}, outgoing: {}, instanceStatus: {}, waking: {},
|
|
46
|
-
prompts: {}, workingSince: {}, stopping: {}, cancelling: {}, scrollMemo: {}, historyRead: new Set(), allHidden: new Set(), rbBusy: new Set(),
|
|
48
|
+
prompts: {}, workingSince: {}, stopping: {}, cancelling: {}, scrollMemo: {}, historyRead: new Set(), gapped: new Set(), allHidden: new Set(), rbBusy: new Set(),
|
|
47
49
|
pollMine: readPollMine(),
|
|
48
50
|
// #1823 (#1828 review): the server's clock minus this device's. A poll's deadline is the server's; a page whose clock
|
|
49
51
|
// is a few seconds off would count a 10 s poll wrong. Only the server's explicit `clock` sets it (#1830 review): a
|
|
@@ -161,6 +163,9 @@ export function createChatStore(deps) {
|
|
|
161
163
|
async function openHistory(name, lease, signal) {
|
|
162
164
|
if (s.historyRead.has(name) || signal?.aborted) return false;
|
|
163
165
|
s.historyRead.add(name);
|
|
166
|
+
// M-2260: after a gap, what this chat held when the read began may sit across a hole: the answer replaces it, and
|
|
167
|
+
// only what arrived while the read was out (the live stream) is kept beside it — exactly what a reload would show.
|
|
168
|
+
const stale = s.gapped.has(name) ? new Set((s.msgs[name] || []).map(msgKeyOf)) : null;
|
|
164
169
|
try {
|
|
165
170
|
const r = await api("GET", `/ui/history?instance=${encodeURIComponent(name)}&limit=200`, undefined, signal);
|
|
166
171
|
if (signal?.aborted) { s.historyRead.delete(name); return false; }
|
|
@@ -169,7 +174,10 @@ export function createChatStore(deps) {
|
|
|
169
174
|
// #2848: a guest's chat whose every message is hidden from it (between agents): its empty chat says so.
|
|
170
175
|
if (r.allHidden === true) s.allHidden.add(name); else s.allHidden.delete(name);
|
|
171
176
|
for (const m of r.messages) liveReplies.message(m);
|
|
172
|
-
|
|
177
|
+
if (stale && s.gapped.has(name)) {
|
|
178
|
+
s.gapped.delete(name);
|
|
179
|
+
s.msgs[name] = R().mergeMessages((s.msgs[name] || []).filter((m) => !stale.has(msgKeyOf(m))), r.messages, 500);
|
|
180
|
+
} else s.msgs[name] = R().mergeMessages(s.msgs[name], r.messages, 500);
|
|
173
181
|
settleOutgoing(name);
|
|
174
182
|
changed(name, "msgs");
|
|
175
183
|
} catch { s.historyRead.delete(name); /* the live stream still works; the next visit tries again */ }
|
|
@@ -557,8 +565,17 @@ export function createChatStore(deps) {
|
|
|
557
565
|
/** Whether a command can start for `target` now (none is out): checked before a typed command line is consumed. */
|
|
558
566
|
const commandFree = (target) => !(s.commands[target] && s.commands[target].busy);
|
|
559
567
|
|
|
568
|
+
/**
|
|
569
|
+
* M-2260: the catch-up after a long absence brought only the newest messages ("gap"): every chat this page holds may
|
|
570
|
+
* have a hole. Each reads its newest history again — the one on screen now, the others when they are next opened.
|
|
571
|
+
*/
|
|
572
|
+
function onGap() {
|
|
573
|
+
for (const name of new Set([...Object.keys(s.msgs), ...s.historyRead])) { s.historyRead.delete(name); s.gapped.add(name); }
|
|
574
|
+
if (s.current) void openHistory(s.current);
|
|
575
|
+
}
|
|
560
576
|
/** Wire the store to the app's stream, for the life of the page. */
|
|
561
577
|
function attach(stream) {
|
|
578
|
+
stream.on("gap", onGap);
|
|
562
579
|
stream.on("status", applyStatus);
|
|
563
580
|
stream.on("activity", applyActivity);
|
|
564
581
|
stream.on("message", ingest);
|
|
@@ -578,7 +595,7 @@ export function createChatStore(deps) {
|
|
|
578
595
|
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
|
|
579
596
|
setCurrent(name) { s.current = name; },
|
|
580
597
|
onAgentReply: liveReplies.subscribe,
|
|
581
|
-
attach, applyStatus, applyActivity, ingest, applyDeliveries, openHistory,
|
|
598
|
+
attach, applyStatus, applyActivity, ingest, applyDeliveries, openHistory, onGap,
|
|
582
599
|
onPrompt, applyPrompts, resolvePrompt, answerPrompt, answerByNonce, holdPrompt, releasePrompt, promptsFor,
|
|
583
600
|
applyReplyButtons, clickReplyButton,
|
|
584
601
|
uploadFile, send, retryOutgoing, deleteOutgoing, putBackOutgoing, threadRows, addFiles, removeFile, attachPastedText, fileBackAsText, isPasted, setDraft,
|
package/dist/ui/fleet-canvas.js
CHANGED
|
@@ -207,7 +207,8 @@ const HEAD_REF_X = 10 - (10 * HEAD.BACK) / HEAD.LEN;
|
|
|
207
207
|
/**
|
|
208
208
|
* The key to the lines (#2052): a **Key** toggle in the toolbar; open, its rows are a strip between the toolbar and the
|
|
209
209
|
* canvas — never over the canvas, so it covers no card (it used to sit in the canvas's bottom-left corner, over the
|
|
210
|
-
* team there at the opening view). Folded until the person opens it; kept per device.
|
|
210
|
+
* team there at the opening view). Folded until the person opens it; kept per device. The lines' key: the Office has its
|
|
211
|
+
* own, for the items it carries (fleet-office.js OfficeKey, #2946).
|
|
211
212
|
*/
|
|
212
213
|
function readKeyOpen() { try { return localStorage.getItem(KEY_KEY) === "1"; } catch { return false; } }
|
|
213
214
|
function KeyToggle({ open, onToggle }) {
|
|
@@ -490,13 +491,13 @@ export function OrgCanvas({ org, app, lease }) {
|
|
|
490
491
|
${mode !== "list" ? html`<label class="s-search oc-find"><${Icon} name="search" size=${14} /><span class="sr-only">${tc("find")}</span>
|
|
491
492
|
<input type="search" value=${query} placeholder=${tc("findShort")} autocomplete="off" onInput=${(e) => setQuery(e.target.value)} onKeyDown=${onFind} />
|
|
492
493
|
${q ? html`<span class="oc-found" role="status">${found ? (found === 1 ? tc("foundOne") : tc("found", found)) : tc("noMatch")}</span>` : null}</label>` : null}
|
|
493
|
-
${mode
|
|
494
|
+
${mode === "canvas" && live ? html`<${KeyToggle} open=${keyOpen} onToggle=${toggleKey} />` : null}
|
|
494
495
|
<button type="button" class=${`btn btn-sm oc-live${live ? " on" : ""}`} aria-pressed=${live ? "true" : "false"} title=${live ? tc("liveOn") : tc("liveOff")}
|
|
495
496
|
onClick=${() => setLive((v) => !v)}><span class="oc-live-dot" aria-hidden="true"></span>${tc("live")}</button>
|
|
496
497
|
<div class="seg-inline" role="group" aria-label=${tc("view")}>${modeBtn("canvas")}${modeBtn("list")}${modeBtn("office")}</div>
|
|
497
498
|
</div>
|
|
498
499
|
</div>
|
|
499
|
-
${mode
|
|
500
|
+
${mode === "canvas" && live && keyOpen ? html`<${Legend} />` : null}
|
|
500
501
|
${mode === "list" ? html`<${OrgChart} org=${org} app=${app} bare=${true} sending=${sending} />`
|
|
501
502
|
: mode === "office" ? html`<${Office} view=${view} traffic=${live ? traffic : null} q=${q} lease=${lease} onOpen=${openChat} />`
|
|
502
503
|
: html`<${Canvas} view=${view} folded=${folded} onFold=${onFold} q=${q} jump=${jump} traffic=${live ? traffic : null} />`}
|
|
@@ -322,47 +322,80 @@ export function along(pts, t) {
|
|
|
322
322
|
// ── Traffic: an item carried from desk to desk ──
|
|
323
323
|
|
|
324
324
|
/** px per ms along the path, the shortest and longest trip, the stagger between items that arrive together. */
|
|
325
|
-
|
|
325
|
+
/**
|
|
326
|
+
* The items' flight: SPEED px/ms along the corridors, at least MIN_MS (#2946: under a second read as a glitch) and at
|
|
327
|
+
* most MAX_MS in the air; a burst's trips start STAGGER_MS apart. At most MAX are in the air or waiting to start: a
|
|
328
|
+
* burst over that flies its newest and drops the rest (they are on the Traffic view), so the room never lags behind a
|
|
329
|
+
* backlog of old events, and a trip under way is never cut short to make room.
|
|
330
|
+
*/
|
|
331
|
+
export const FLIGHT = { SPEED: 0.35, MIN_MS: 2500, MAX_MS: 4500, STAGGER_MS: 200, MAX: 12 };
|
|
326
332
|
const MAX_SEEN = 2000;
|
|
333
|
+
/** A trip's time in the air for a path `len` px long. */
|
|
334
|
+
export const flightMs = (len) => Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, len / FLIGHT.SPEED));
|
|
335
|
+
/** How many new items may wait for their first frame (a frame not drawn for a while: the newest kept). */
|
|
336
|
+
const MAX_WAITING = 200;
|
|
327
337
|
/**
|
|
328
|
-
* The
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
338
|
+
* The trips' lifecycle, on whatever `pathOf(on, from, to)` routes them through (`on`: what `frame` is given — a layout,
|
|
339
|
+
* or All offices' tiles; null: not drawn there). `sync(items, now, still)`: each new item is seen once (`seen`,
|
|
340
|
+
* bounded: never flown again) and waits for the next frame — none when `still` (reduced motion) or for a stale
|
|
341
|
+
* delegation. `frame(now, on)`: first the waiting are admitted, #2946 — only those with a path here (all controllers
|
|
342
|
+
* get the whole fleet's traffic: the rest are not this one's to fly, and never take a place), and of those the newest
|
|
343
|
+
* that fit in `max` beside the trips still up (landed ones and those with no path any more pruned first), each batch
|
|
344
|
+
* staggered from when it came, one already past its flight time never flown late; then the trips under way
|
|
345
|
+
* { id, hue, kind, from, x, y, t }. A trip under way
|
|
346
|
+
* is never cut short to make room. `size()`: trips up or waiting.
|
|
333
347
|
*/
|
|
334
|
-
export function
|
|
348
|
+
export function createTrips(pathOf, { max = FLIGHT.MAX } = {}) {
|
|
335
349
|
const seen = new Set();
|
|
336
|
-
let trips = [];
|
|
350
|
+
let trips = [], waiting = [];
|
|
351
|
+
const route = (f, on) => { f.on = on; f.pts = pathOf(on, f.from, f.to); f.ms = f.pts ? flightMs(pathLength(f.pts)) : 0; };
|
|
337
352
|
return {
|
|
338
353
|
sync(items, now, still = false) {
|
|
339
|
-
let
|
|
354
|
+
let fresh = 0;
|
|
340
355
|
for (const it of items || []) {
|
|
341
356
|
if (seen.has(it.id)) continue;
|
|
342
357
|
seen.add(it.id);
|
|
343
358
|
if (seen.size > MAX_SEEN) seen.delete(seen.values().next().value);
|
|
344
359
|
if (still || it.phase === "stale") continue;
|
|
345
|
-
|
|
346
|
-
|
|
360
|
+
waiting.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), at: now });
|
|
361
|
+
fresh++;
|
|
347
362
|
}
|
|
348
|
-
if (
|
|
349
|
-
return
|
|
363
|
+
if (waiting.length > MAX_WAITING) waiting = waiting.slice(-MAX_WAITING);
|
|
364
|
+
return fresh;
|
|
350
365
|
},
|
|
351
|
-
frame(now,
|
|
352
|
-
|
|
366
|
+
frame(now, on) {
|
|
367
|
+
// First the trips up: routed again on a new layout, the ones with no path or landed dropped — so the room below is
|
|
368
|
+
// what is really free (#2977 review: after a gap with no frame, a hidden tab, landed trips held every place).
|
|
353
369
|
trips = trips.filter((f) => {
|
|
354
|
-
if (f.
|
|
355
|
-
|
|
356
|
-
const t = (now - f.start) / f.ms;
|
|
357
|
-
if (t >= 1) return false;
|
|
358
|
-
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 }); }
|
|
359
|
-
return true;
|
|
370
|
+
if (f.on !== on) route(f, on);
|
|
371
|
+
return !!f.pts && (now - f.start) / f.ms < 1;
|
|
360
372
|
});
|
|
373
|
+
if (waiting.length) {
|
|
374
|
+
// Then the waiting with a path here and still due (one whose own flight time has passed since it came is stale:
|
|
375
|
+
// never flown late), the newest that fit, each batch staggered from when it came.
|
|
376
|
+
const ok = [];
|
|
377
|
+
for (const w of waiting) { const f = { ...w, start: w.at }; route(f, on); if (f.pts && now - w.at < f.ms) ok.push(f); }
|
|
378
|
+
waiting = [];
|
|
379
|
+
const room = max - trips.length, add = room > 0 ? ok.slice(-room) : []; // never slice(-0): that is all of them
|
|
380
|
+
const k = new Map();
|
|
381
|
+
for (const f of add) { const n = k.get(f.at) || 0; k.set(f.at, n + 1); f.start = f.at + n * FLIGHT.STAGGER_MS; trips.push(f); }
|
|
382
|
+
}
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const f of trips) {
|
|
385
|
+
const t = (now - f.start) / f.ms;
|
|
386
|
+
if (t >= 0 && t < 1) { 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 }); }
|
|
387
|
+
}
|
|
361
388
|
return out;
|
|
362
389
|
},
|
|
363
|
-
size: () => trips.length,
|
|
390
|
+
size: () => trips.length + waiting.length,
|
|
364
391
|
};
|
|
365
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* The items in the air in one office: each new one (a message, a delegation's work, its report coming back) carried from
|
|
395
|
+
* its sender's desk to its receiver's along layout L's corridors (createTrips; `frame(now, L)`; `from`: who sent it — a
|
|
396
|
+
* pack can draw it as theirs). A trip whose desks are not drawn on L is not this office's.
|
|
397
|
+
*/
|
|
398
|
+
export const createFlights = (opts = {}) => createTrips((L, from, to) => (L ? walkPath(L, from, to) : null), opts);
|
|
366
399
|
|
|
367
400
|
/** How many delegations each instance is working on now (the folders on its desk). */
|
|
368
401
|
export function workOn(items) {
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
// contain); a tie keeps the narrower. The same input always gives the same picture.
|
|
14
14
|
// - Traffic between people in different tiles is a straight flight between their seats in overview coordinates
|
|
15
15
|
// (createCrossFlights); between two people in the same tile it is that tile's own (createFlights, along its corridors).
|
|
16
|
-
import { FLIGHT,
|
|
17
|
-
import { hue } from "./fleet-traffic.js";
|
|
16
|
+
import { FLIGHT, createTrips, desksOf, membersOf } from "./fleet-office-model.js";
|
|
18
17
|
|
|
19
18
|
/** The switcher's "All offices" value: '*' is never in a stored office's id (fleet-office-model.js officeOk). */
|
|
20
19
|
export const ALL_ID = "*all";
|
|
@@ -127,41 +126,10 @@ export function crossPath(parts, from, to) {
|
|
|
127
126
|
return a && z ? [a, z] : null;
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
const MAX_SEEN = 2000;
|
|
131
129
|
/**
|
|
132
|
-
* The items in the air between tiles — fleet-office-model.js
|
|
133
|
-
*
|
|
134
|
-
* t } in overview units;
|
|
130
|
+
* The items in the air between tiles — fleet-office-model.js createTrips (each new item once; none for a stale delegation
|
|
131
|
+
* or with reduced motion; at most `max` at once, newest first, of those with a crossing), on crossPath's straight line:
|
|
132
|
+
* `frame(now, parts)` → { id, hue, kind, from, x, y, t } in overview units; one with no crossing (both in one tile, or
|
|
133
|
+
* either gone) is not this one's.
|
|
135
134
|
*/
|
|
136
|
-
export
|
|
137
|
-
const seen = new Set();
|
|
138
|
-
let trips = [];
|
|
139
|
-
return {
|
|
140
|
-
sync(items, now, still = false) {
|
|
141
|
-
let k = 0, started = 0;
|
|
142
|
-
for (const it of items || []) {
|
|
143
|
-
if (seen.has(it.id)) continue;
|
|
144
|
-
seen.add(it.id);
|
|
145
|
-
if (seen.size > MAX_SEEN) seen.delete(seen.values().next().value);
|
|
146
|
-
if (still || it.phase === "stale") continue;
|
|
147
|
-
trips.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now + k++ * FLIGHT.STAGGER_MS, parts: null, pts: null, ms: 0 });
|
|
148
|
-
started++;
|
|
149
|
-
}
|
|
150
|
-
if (trips.length > max) trips = trips.slice(trips.length - max);
|
|
151
|
-
return started;
|
|
152
|
-
},
|
|
153
|
-
frame(now, parts) {
|
|
154
|
-
const out = [];
|
|
155
|
-
trips = trips.filter((f) => {
|
|
156
|
-
if (f.parts !== parts) { f.parts = parts; f.pts = crossPath(parts, f.from, f.to); f.ms = f.pts ? Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(f.pts) / FLIGHT.SPEED)) : 0; }
|
|
157
|
-
if (!f.pts) return false;
|
|
158
|
-
const t = (now - f.start) / f.ms;
|
|
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, from: f.from, x: p.x, y: p.y, t }); }
|
|
161
|
-
return true;
|
|
162
|
-
});
|
|
163
|
-
return out;
|
|
164
|
-
},
|
|
165
|
-
size: () => trips.length,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
135
|
+
export const createCrossFlights = ({ max = FLIGHT.MAX } = {}) => createTrips((parts, from, to) => (parts ? crossPath(parts, from, to) : null), { max });
|
|
@@ -154,10 +154,11 @@ const SET_STRIPS = [...POSES, "prop"];
|
|
|
154
154
|
const WALKS = ["walk", "walkDown", "walkUp", "walkSide"];
|
|
155
155
|
const BUILT_IN = new Set(["pixel", "studio"]);
|
|
156
156
|
/**
|
|
157
|
-
* Ids built in after packs could be uploaded under them (#2631: "cozy"): a new upload is
|
|
158
|
-
* kept under one before (`kept`) is still read back, listed and deletable, and wins the id
|
|
157
|
+
* Ids built in after packs could be uploaded under them (#2631: "cozy"; #2947: "guild", "school"): a new upload is
|
|
158
|
+
* refused, but a pack the host kept under one before (`kept`) is still read back, listed and deletable, and wins the id
|
|
159
|
+
* for the offices that name it.
|
|
159
160
|
*/
|
|
160
|
-
const RESERVED = new Set(["cozy"]);
|
|
161
|
+
const RESERVED = new Set(["cozy", "guild", "school"]);
|
|
161
162
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
162
163
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
163
164
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The Office view's art (fleet-office.js draws with it on a <canvas>). A skin draws a layout that fleet-office-model.js
|
|
2
|
-
// (desks) or fleet-office-zones.js (zones) made.
|
|
2
|
+
// (desks) or fleet-office-zones.js (zones) made. Five skins are built in. Two draw everything procedurally — little
|
|
3
3
|
// pixel people, desks, lamps: "pixel" (the desks layout: a room per team, a desk per instance) and "studio" (the zones
|
|
4
4
|
// layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their state). The third,
|
|
5
5
|
// "cozy" (#2631), is the zones layout on one picture shipped with the page (a warm pixel-art office), the same built-in
|
|
6
|
-
// people in it
|
|
6
|
+
// people in it; "guild" and "school" (#2947) are two more such pictures on the cozy's arrangement. Every drawing call
|
|
7
|
+
// goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
|
|
7
8
|
// format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
|
|
8
9
|
// drawing. An admin uploads a pack (#2513, the host keeps it: src/web-office-skins.ts); loadPackSkin() reads it back —
|
|
9
10
|
// its manifest checked here again (checkSkinPack), its pictures by their content ids — and spriteSkin() draws it.
|
|
@@ -243,13 +244,21 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
243
244
|
if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
const Q = 2; // a prop's art pixel
|
|
247
|
+
const Q = 2; // a prop's art pixel, before PROP_SCALE
|
|
248
|
+
/**
|
|
249
|
+
* #2946: the built-in props are drawn this much larger than their 16 px art (a pack's own `props` keep their size). In
|
|
250
|
+
* the scene's units, so they scale with the room as its people do (zoom, a phone, an All offices tile).
|
|
251
|
+
*/
|
|
252
|
+
export const PROP_SCALE = 1.75;
|
|
247
253
|
function drawProp(ctx, it, ms, env) {
|
|
248
|
-
const c = env.pal.fx[it.hue] || env.pal.fx.send,
|
|
249
|
-
const b = (c2, r, w, h, col) => px(ctx,
|
|
254
|
+
const c = env.pal.fx[it.hue] || env.pal.fx.send, lift = Math.sin(Math.PI * it.t) * 10;
|
|
255
|
+
const b = (c2, r, w, h, col) => px(ctx, -8 + c2 * Q, -8 + r * Q, w * Q, h * Q, col);
|
|
250
256
|
ctx.save();
|
|
257
|
+
ctx.translate(it.x, it.y);
|
|
258
|
+
ctx.scale(PROP_SCALE, PROP_SCALE);
|
|
251
259
|
ctx.fillStyle = "rgba(0, 0, 0, 0.18)";
|
|
252
|
-
ctx.beginPath(); ctx.ellipse(
|
|
260
|
+
ctx.beginPath(); ctx.ellipse(0, 4, 7, 2.5, 0, 0, Math.PI * 2); ctx.fill();
|
|
261
|
+
ctx.translate(0, -lift);
|
|
253
262
|
if (it.hue === "task") { b(0, 0, 4, 2, c); b(0, 1, 8, 6, c); b(0, 2, 8, 1, "rgba(255, 255, 255, 0.35)"); }
|
|
254
263
|
else if (it.hue === "report") { b(1, 0, 6, 8, "#fbfbfb"); b(1, 0, 6, 1, c); b(2, 4, 1, 1, c); b(3, 5, 1, 1, c); b(4, 4, 1, 1, c); b(5, 3, 1, 1, c); }
|
|
255
264
|
else if (it.hue === "broadcast") { b(0, 3, 2, 2, c); b(2, 2, 2, 4, c); b(4, 1, 2, 6, c); if (frameAt(ms, 2, 6)) { b(7, 1, 1, 2, c); b(7, 5, 1, 2, c); } else b(7, 3, 1, 2, c); }
|
|
@@ -645,28 +654,112 @@ export const COZY_PACK = {
|
|
|
645
654
|
stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
|
|
646
655
|
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
647
656
|
};
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
657
|
+
// The cozy's zones on every picture-backed built-in: work, lounge, the alert corner, the nap corner.
|
|
658
|
+
const PICTURE_ZONES = { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" };
|
|
659
|
+
const wps = (list) => list.map(([id, x, y]) => ({ id, x, y }));
|
|
660
|
+
|
|
661
|
+
// ── #2947: two more on the cozy's arrangement — work top-left, the lounge top-right, the nap corner bottom-left, the
|
|
662
|
+
// alert corner bottom-right, one straight aisle up the middle from a bottom-centre door ──
|
|
663
|
+
|
|
664
|
+
/** The guild: a fantasy adventurers' guild hall (src/ui/shared/office/guild.png). */
|
|
665
|
+
export const GUILD_BACKGROUND = "/assets/office-guild.png";
|
|
666
|
+
// Seats: the chairs at the six map desks, the tavern bench, the stools round the table, the bar; the inn beds (a sleeper's
|
|
667
|
+
// middle); in front of the quest board.
|
|
668
|
+
const GUILD_AREAS = {
|
|
669
|
+
desks: { name: "desks", rect: { x: 130, y: 190, w: 400, h: 160 }, seats: pts([180, 200], [320, 200], [450, 200], [180, 315], [320, 315], [450, 315]) },
|
|
670
|
+
lounge: { name: "lounge", rect: { x: 780, y: 190, w: 440, h: 150 }, seats: pts([840, 200], [900, 200], [810, 290], [935, 290], [870, 325], [1040, 300]) },
|
|
671
|
+
alert: { name: "alert", rect: { x: 860, y: 560, w: 260, h: 50 }, seats: pts([900, 590], [1000, 590], [1090, 590]) },
|
|
672
|
+
nap: { name: "nap", rect: { x: 110, y: 540, w: 380, h: 80 }, seats: pts([160, 560], [285, 560], [405, 560]) },
|
|
673
|
+
};
|
|
674
|
+
const GUILD_WAYPOINTS = wps([
|
|
675
|
+
["door", 640, 650], ["low", 640, 615], ["hall", 640, 345], ["top", 640, 130],
|
|
676
|
+
// the desks: the aisle under the second row (y 345), the one between the rows (y 225), the column beside them (x 540)
|
|
677
|
+
["d-east", 540, 345], ["d2-3", 450, 345], ["d2-2", 320, 345], ["d2-1", 180, 345],
|
|
678
|
+
["d-up", 540, 225], ["d1-3", 450, 225], ["d1-2", 320, 225], ["d1-1", 180, 225],
|
|
679
|
+
// the beds and the quest board: the aisle along the bottom (y 615)
|
|
680
|
+
["n-x", 500, 615], ["n-3", 405, 615], ["n-2", 285, 615], ["n-1", 160, 615],
|
|
681
|
+
["a-1", 900, 615], ["a-2", 1000, 615], ["a-3", 1090, 615],
|
|
682
|
+
// the tavern: under the round table (y 345), up the bench's west end (x 770) to in front of it (y 215)
|
|
683
|
+
["l-hall", 770, 345], ["l-tw", 810, 345], ["l-ts", 870, 345], ["l-te", 935, 345], ["l-bar", 1040, 345],
|
|
684
|
+
["l-up", 770, 215], ["l-b1", 840, 215], ["l-b2", 900, 215],
|
|
685
|
+
]);
|
|
686
|
+
const GUILD_EDGES = [
|
|
687
|
+
["door", "low"], ["low", "hall"], ["hall", "top"],
|
|
688
|
+
["hall", "d-east"], ["d-east", "d2-3"], ["d2-3", "d2-2"], ["d2-2", "d2-1"], ["d-east", "d-up"], ["d-up", "d1-3"], ["d1-3", "d1-2"], ["d1-2", "d1-1"],
|
|
689
|
+
["low", "n-x"], ["n-x", "n-3"], ["n-3", "n-2"], ["n-2", "n-1"], ["low", "a-1"], ["a-1", "a-2"], ["a-2", "a-3"],
|
|
690
|
+
["hall", "l-hall"], ["l-hall", "l-tw"], ["l-tw", "l-ts"], ["l-ts", "l-te"], ["l-te", "l-bar"], ["l-hall", "l-up"], ["l-up", "l-b1"], ["l-b1", "l-b2"],
|
|
691
|
+
];
|
|
692
|
+
export const GUILD_PACK = {
|
|
693
|
+
format: 1, id: "guild", scale: 1, background: GUILD_BACKGROUND, areas: GUILD_AREAS, door: { x: 640, y: 690 },
|
|
694
|
+
waypoints: GUILD_WAYPOINTS, edges: GUILD_EDGES, straight: true, furniture: [], stateZones: PICTURE_ZONES,
|
|
695
|
+
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
/** The school: a classroom (src/ui/shared/office/school.png). */
|
|
699
|
+
export const SCHOOL_BACKGROUND = "/assets/office-school.png";
|
|
700
|
+
// Seats: the chairs at the six student desks; the sofa, the two stools, the vending machine; the nurse's beds; in front
|
|
701
|
+
// of the chalkboard.
|
|
702
|
+
const SCHOOL_AREAS = {
|
|
703
|
+
desks: { name: "desks", rect: { x: 130, y: 205, w: 400, h: 140 }, seats: pts([190, 215], [325, 215], [455, 215], [170, 320], [315, 320], [450, 320]) },
|
|
704
|
+
lounge: { name: "lounge", rect: { x: 770, y: 125, w: 420, h: 110 }, seats: pts([850, 135], [925, 135], [1000, 135], [810, 225], [1035, 225], [1145, 185]) },
|
|
705
|
+
alert: { name: "alert", rect: { x: 840, y: 560, w: 260, h: 50 }, seats: pts([880, 590], [965, 590], [1050, 590]) },
|
|
706
|
+
nap: { name: "nap", rect: { x: 110, y: 540, w: 380, h: 80 }, seats: pts([160, 555], [295, 555], [400, 555]) },
|
|
707
|
+
};
|
|
708
|
+
const SCHOOL_WAYPOINTS = wps([
|
|
709
|
+
["door", 640, 660], ["low", 640, 625], ["hall", 640, 335], ["mid", 640, 280], ["top", 640, 100],
|
|
710
|
+
// the desks: the aisle under the second row (y 335), the one between the rows (y 232), the column beside them (x 540)
|
|
711
|
+
["d-east", 540, 335], ["d2-3", 450, 335], ["d2-2", 315, 335], ["d2-1", 170, 335],
|
|
712
|
+
["d-up", 540, 232], ["d1-3", 455, 232], ["d1-2", 325, 232], ["d1-1", 190, 232],
|
|
713
|
+
// the beds and the chalkboard: the aisle along the bottom (y 625)
|
|
714
|
+
["n-x", 520, 625], ["n-3", 400, 625], ["n-2", 295, 625], ["n-1", 160, 625],
|
|
715
|
+
["a-1", 880, 625], ["a-2", 965, 625], ["a-3", 1050, 625],
|
|
716
|
+
// the lounge: under the rug (y 280) to the stools and the machine, up its west edge (x 760) to in front of the sofa (y 150)
|
|
717
|
+
["l-hall", 760, 280], ["l-w", 810, 280], ["l-e", 1035, 280], ["l-vend", 1145, 280],
|
|
718
|
+
["l-up", 760, 150], ["l-s1", 850, 150], ["l-s2", 925, 150], ["l-s3", 1000, 150],
|
|
719
|
+
]);
|
|
720
|
+
const SCHOOL_EDGES = [
|
|
721
|
+
["door", "low"], ["low", "hall"], ["hall", "mid"], ["mid", "top"],
|
|
722
|
+
["hall", "d-east"], ["d-east", "d2-3"], ["d2-3", "d2-2"], ["d2-2", "d2-1"], ["d-east", "d-up"], ["d-up", "d1-3"], ["d1-3", "d1-2"], ["d1-2", "d1-1"],
|
|
723
|
+
["low", "n-x"], ["n-x", "n-3"], ["n-3", "n-2"], ["n-2", "n-1"], ["low", "a-1"], ["a-1", "a-2"], ["a-2", "a-3"],
|
|
724
|
+
["mid", "l-hall"], ["l-hall", "l-w"], ["l-w", "l-e"], ["l-e", "l-vend"], ["l-hall", "l-up"], ["l-up", "l-s1"], ["l-s1", "l-s2"], ["l-s2", "l-s3"],
|
|
725
|
+
];
|
|
726
|
+
export const SCHOOL_PACK = {
|
|
727
|
+
format: 1, id: "school", scale: 1, background: SCHOOL_BACKGROUND, areas: SCHOOL_AREAS, door: { x: 640, y: 700 },
|
|
728
|
+
waypoints: SCHOOL_WAYPOINTS, edges: SCHOOL_EDGES, straight: true, furniture: [], stateZones: PICTURE_ZONES,
|
|
729
|
+
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The built-ins drawn from one picture shipped with the page (#2631 cozy, #2947 guild and school): each one's pack, and
|
|
734
|
+
* until its picture is in, the studio's floors under its areas (the bare skin); `job` is the picture's load in flight.
|
|
735
|
+
*/
|
|
736
|
+
const PICTURES = new Map([COZY_PACK, GUILD_PACK, SCHOOL_PACK].map((pack) => [pack.id, { pack, bare: spriteSkin(pack, new Map(), studioSkin), job: null }]));
|
|
737
|
+
/** Their ids: a style naming one loads its picture (loadBuiltInSkin). */
|
|
738
|
+
export const PICTURE_SKINS = [...PICTURES.keys()];
|
|
651
739
|
|
|
652
740
|
/** The skins built into this page; an uploaded pack is loaded by loadPackSkin. */
|
|
653
|
-
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin], [
|
|
654
|
-
export const BUILT_IN_SKINS = ["pixel", "studio",
|
|
741
|
+
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin], ...[...PICTURES].map(([id, p]) => [id, p.bare])]);
|
|
742
|
+
export const BUILT_IN_SKINS = ["pixel", "studio", ...PICTURE_SKINS];
|
|
655
743
|
/**
|
|
656
|
-
* #2631:
|
|
657
|
-
* skin, or null while it cannot load (the bare one stays; tried again next time)
|
|
744
|
+
* #2631/#2947: a picture-backed built-in's picture, loaded once; the skin drawn from it then takes the bare one's place
|
|
745
|
+
* in SKINS (skinFor). The skin, or null while it cannot load (the bare one stays; tried again next time); null for an id
|
|
746
|
+
* that is not one of them.
|
|
658
747
|
*/
|
|
659
|
-
export function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
748
|
+
export function loadBuiltInSkin(id, { image = defaultImage } = {}) {
|
|
749
|
+
const p = PICTURES.get(id);
|
|
750
|
+
if (!p) return Promise.resolve(null);
|
|
751
|
+
if (SKINS.get(id) !== p.bare) return Promise.resolve(SKINS.get(id));
|
|
752
|
+
p.job ??= image(p.pack.background).then((im) => {
|
|
753
|
+
p.job = null;
|
|
663
754
|
if (!im) return null;
|
|
664
|
-
const sk = spriteSkin(
|
|
665
|
-
SKINS.set(
|
|
755
|
+
const sk = spriteSkin(p.pack, new Map([[p.pack.background, im]]), studioSkin);
|
|
756
|
+
SKINS.set(id, sk);
|
|
666
757
|
return sk;
|
|
667
|
-
}, () => {
|
|
668
|
-
return
|
|
758
|
+
}, () => { p.job = null; return null; });
|
|
759
|
+
return p.job;
|
|
669
760
|
}
|
|
761
|
+
/** #2631: the cozy's picture (loadBuiltInSkin for "cozy"). */
|
|
762
|
+
export const loadCozySkin = (opts = {}) => loadBuiltInSkin("cozy", opts);
|
|
670
763
|
/** A skin by id: a built-in one, else a pack loaded here (`packs`: id → skin), else the default (pixel). */
|
|
671
764
|
export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
|
|
672
765
|
|
|
@@ -19,6 +19,8 @@ import { levelsOk } from "./fleet-office-levels.js";
|
|
|
19
19
|
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";
|
|
20
20
|
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
21
21
|
const LOBBY_KEY = "agend_org_office_lobby";
|
|
22
|
+
/** #2946: whether the key to the items in the air is open here (default: folded). */
|
|
23
|
+
const KEY_OPEN_KEY = "agend_org_office_key";
|
|
22
24
|
/** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
|
|
23
25
|
const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
24
26
|
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
@@ -195,6 +197,9 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
195
197
|
saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
|
|
196
198
|
/** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
|
|
197
199
|
lobbyTile: () => read(LOBBY_KEY) !== "0",
|
|
200
|
+
/** #2946: whether the key to the items carried between desks is open here (default: folded). */
|
|
201
|
+
keyOpen: () => read(KEY_OPEN_KEY) === "1",
|
|
202
|
+
saveKeyOpen: (v) => write(KEY_OPEN_KEY, v ? "1" : "0"),
|
|
198
203
|
saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
|
|
199
204
|
/** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
|
|
200
205
|
view: (key) => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// changes it walks (along the pack's waypoints) to the new area. Pure — fleet-office.js draws it. Also here: what a
|
|
4
4
|
// character says in its speech bubble, and which furniture is active.
|
|
5
5
|
import { LOGICAL, own } from "./fleet-office-pack.js";
|
|
6
|
-
import {
|
|
6
|
+
import { flightMs, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
|
|
7
7
|
import { hue } from "./fleet-traffic.js";
|
|
8
8
|
|
|
9
9
|
/** The area for a state: the pack's own, else its "default", else its first area. */
|
|
@@ -281,7 +281,7 @@ export function createMail({ max = MAIL.MAX } = {}) {
|
|
|
281
281
|
if (!drop) continue; // nowhere off the seats: not delivered here
|
|
282
282
|
const pts = [{ x: L.door.x, y: L.door.y }, drop];
|
|
283
283
|
letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
|
|
284
|
-
ms:
|
|
284
|
+
ms: flightMs(pathLength(pts)) });
|
|
285
285
|
started++;
|
|
286
286
|
}
|
|
287
287
|
if (letters.length > max) letters = letters.slice(letters.length - max);
|