@songsid/agend 2.3.4-beta.3 → 2.3.4-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/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 +0 -2
- package/dist/backend/grok.js +0 -22
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +0 -2
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +10 -1
- package/dist/daemon.js +122 -80
- 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/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/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 +3 -0
- 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-skin.js +12 -4
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +31 -1
- 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 +6 -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/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/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 });
|
|
@@ -244,13 +244,21 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
244
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(); }
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
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;
|
|
248
253
|
function drawProp(ctx, it, ms, env) {
|
|
249
|
-
const c = env.pal.fx[it.hue] || env.pal.fx.send,
|
|
250
|
-
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);
|
|
251
256
|
ctx.save();
|
|
257
|
+
ctx.translate(it.x, it.y);
|
|
258
|
+
ctx.scale(PROP_SCALE, PROP_SCALE);
|
|
252
259
|
ctx.fillStyle = "rgba(0, 0, 0, 0.18)";
|
|
253
|
-
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);
|
|
254
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)"); }
|
|
255
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); }
|
|
256
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); }
|
|
@@ -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);
|
package/dist/ui/fleet-office.js
CHANGED
|
@@ -64,7 +64,7 @@ import { nodeText, stateWords } from "./fleet-org.js";
|
|
|
64
64
|
import { hits, needle } from "./fleet-canvas-layout.js";
|
|
65
65
|
import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
|
|
66
66
|
officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
|
|
67
|
-
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, PICTURE_SKINS, bubbleBox, loadBuiltInSkin, loadPackSkin, modeOf, skinFor, tagBox, tagFootprint } from "./fleet-office-skin.js";
|
|
67
|
+
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, PICTURE_SKINS, bubbleBox, pixelSkin, loadBuiltInSkin, loadPackSkin, modeOf, skinFor, tagBox, tagFootprint } from "./fleet-office-skin.js";
|
|
68
68
|
import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
|
|
69
69
|
import { actsOk, createOfficeStore } from "./fleet-office-store.js";
|
|
70
70
|
import { badgeOf, fixedOf } from "./fleet-office-levels.js";
|
|
@@ -96,6 +96,8 @@ register("orgo", {
|
|
|
96
96
|
fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
|
|
97
97
|
fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
|
|
98
98
|
allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
|
|
99
|
+
key: "Key", keyTitle: "What the items carried between desks are", keyLabel: "Carried between desks",
|
|
100
|
+
keyTask: "Folder: a task", keySend: "Envelope: a message", keyReport: "Paper: a report", keyBroadcast: "Horn: a broadcast",
|
|
99
101
|
tileLabel: "{0} ({1})", openOffice: "Open the office {0}", lobbyTile: "Lobby tile (in no office)", lobbyEveryone: "Everyone is in an office", overviewNoEdit: "Pick one office to edit offices or a layout",
|
|
100
102
|
},
|
|
101
103
|
"zh-TW": {
|
|
@@ -114,6 +116,8 @@ register("orgo", {
|
|
|
114
116
|
fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
|
|
115
117
|
fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
|
|
116
118
|
allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
|
|
119
|
+
key: "圖例", keyTitle: "桌子之間傳遞的物品代表什麼", keyLabel: "桌子之間傳遞的物品",
|
|
120
|
+
keyTask: "資料夾:任務", keySend: "信封:訊息", keyReport: "紙張:回報", keyBroadcast: "喇叭:廣播",
|
|
117
121
|
tileLabel: "{0}({1})", openOffice: "開啟辦公室「{0}」", lobbyTile: "大廳格(不在任何辦公室)", lobbyEveryone: "每個成員都已在某間辦公室", overviewNoEdit: "請先選一間辦公室,再編輯辦公室或配置",
|
|
118
122
|
},
|
|
119
123
|
});
|
|
@@ -584,6 +588,28 @@ function People({ nodes, pinned, onChange }) {
|
|
|
584
588
|
</div>`;
|
|
585
589
|
}
|
|
586
590
|
|
|
591
|
+
/** #2946: the key's rows — each kind's item as the scene draws it (the built-in prop, in the theme's colour), and its words. */
|
|
592
|
+
export const KEY_KINDS = [["task", "keyTask"], ["send", "keySend"], ["report", "keyReport"], ["broadcast", "keyBroadcast"]];
|
|
593
|
+
const KEY_PX = 32;
|
|
594
|
+
/**
|
|
595
|
+
* #2946 (hxhsandy): the key to the items carried between desks — a strip under the Office's bar (never over the scene),
|
|
596
|
+
* opened by its Key toggle. The shapes are drawn by the built-in skin, so they are the ones in the air, at their size.
|
|
597
|
+
*/
|
|
598
|
+
export function OfficeKey({ pal }) {
|
|
599
|
+
const paint = (hue) => (cv) => {
|
|
600
|
+
const g = cv && typeof cv.getContext === "function" ? cv.getContext("2d") : null;
|
|
601
|
+
if (!g) return;
|
|
602
|
+
const r = Math.max(1, Math.round(globalThis.devicePixelRatio || 1));
|
|
603
|
+
if (cv.width !== KEY_PX * r) { cv.width = KEY_PX * r; cv.height = KEY_PX * r; }
|
|
604
|
+
g.setTransform(r, 0, 0, r, 0, 0); g.clearRect(0, 0, KEY_PX, KEY_PX);
|
|
605
|
+
try { pixelSkin.drawProp(g, { hue, kind: hue, x: KEY_PX / 2, y: KEY_PX / 2 + 4, t: 0 }, 0, { pal }); } catch { /* left blank */ }
|
|
606
|
+
};
|
|
607
|
+
return html`<div class="oo-key" role="region" aria-label=${to("keyLabel")}>
|
|
608
|
+
<ul class="oo-key-rows">${KEY_KINDS.map(([hue, k]) => html`<li key=${hue} class="oo-key-row" data-kind=${hue}>
|
|
609
|
+
<canvas class="oo-key-icon" width=${KEY_PX} height=${KEY_PX} aria-hidden="true" ref=${paint(hue)}></canvas><span>${to(k)}</span></li>`)}</ul>
|
|
610
|
+
</div>`;
|
|
611
|
+
}
|
|
612
|
+
|
|
587
613
|
/**
|
|
588
614
|
* The view: `view` buildOrgView's answer; `traffic` the live items (null: Live is off); `q` the Find box's needle;
|
|
589
615
|
* `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat; none: a desk opens nothing,
|
|
@@ -609,6 +635,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
609
635
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
610
636
|
// Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
|
|
611
637
|
const [nightOn, setNightOn] = useState(() => store.nightOn());
|
|
638
|
+
const [keyOpen, setKeyOpen] = useState(() => store.keyOpen());
|
|
612
639
|
const [nightNow, setNightNow] = useState(() => isNight());
|
|
613
640
|
useEffect(() => { if (!nightOn) return undefined; setNightNow(isNight()); const h = setInterval(() => setNightNow(isNight()), NIGHT.TICK); return () => clearInterval(h); }, [nightOn]);
|
|
614
641
|
const night = nightOn && nightNow;
|
|
@@ -1458,6 +1485,8 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1458
1485
|
${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
|
|
1459
1486
|
${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
|
|
1460
1487
|
${styleErr && auto ? html`<span class="note oo-note oo-style-err" role="status">${styleErr}</span>` : null}
|
|
1488
|
+
${traffic ? html`<button type="button" class=${`btn btn-sm oo-key-btn${keyOpen ? " on" : ""}`} aria-expanded=${keyOpen ? "true" : "false"} title=${to("keyTitle")}
|
|
1489
|
+
onClick=${() => { const v = !keyOpen; store.saveKeyOpen(v); setKeyOpen(v); }}>${to("key")}</button>` : null}
|
|
1461
1490
|
${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
|
|
1462
1491
|
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1463
1492
|
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
@@ -1465,6 +1494,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1465
1494
|
${canShare ? html`<${ShareButton} offices=${stored} current=${overview ? "all" : custom ? custom.id : null} disabled=${!!layout}
|
|
1466
1495
|
get=${lease ? (path, o) => lease.fetch(path, o) : undefined} />` : null}
|
|
1467
1496
|
</div>
|
|
1497
|
+
${traffic && keyOpen ? html`<${OfficeKey} pal=${pal} />` : null}
|
|
1468
1498
|
${editing && !overview ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
|
|
1469
1499
|
ask=${ask} current=${() => shown.current} onSaved=${adopt} onClose=${() => setEditing(false)} />` : null}
|
|
1470
1500
|
${overview || people.length || L.rooms.length ? null : html`<p class="note oo-empty">${to("nobody")} ${auto && (f.team || f.pinned.length) ? html`<button type="button" class="btn btn-sm" onClick=${() => setF({ team: "", pinned: [] })}>${to("everyone")}</button>` : null}</p>`}
|
package/dist/ui/panel-log.js
CHANGED
|
@@ -49,6 +49,7 @@ register("log", {
|
|
|
49
49
|
user: "Asked", assistant: "Replied", error: "Error", errorCode: "Error {0}", turnEnd: "Turn ended", turnEndIn: "Turn ended · {0}",
|
|
50
50
|
compacted: "Conversation compacted", turn: "Turn",
|
|
51
51
|
running: "running", done: "done", failed: "failed", interrupted: "interrupted", exit: "exit {0}",
|
|
52
|
+
showScreen: "Show what was on screen", hideScreen: "Hide what was on screen",
|
|
52
53
|
showOutput: "Show output", hideOutput: "Hide output", args: "Arguments", output: "Output", noOutput: "No output.",
|
|
53
54
|
showAll: "Show all", showLess: "Show less", cut: "… {0} more characters not shown.", tooLarge: "Output of {0}: only its start is shown.",
|
|
54
55
|
times: "×{0}", newer: "{0} new",
|
|
@@ -87,6 +88,7 @@ register("log", {
|
|
|
87
88
|
user: "收到要求", assistant: "回覆", error: "錯誤", errorCode: "錯誤 {0}", turnEnd: "回合結束", turnEndIn: "回合結束 · {0}",
|
|
88
89
|
compacted: "對話已壓縮", turn: "回合",
|
|
89
90
|
running: "執行中", done: "完成", failed: "失敗", interrupted: "中斷", exit: "結束碼 {0}",
|
|
91
|
+
showScreen: "顯示當時的畫面", hideScreen: "隱藏當時的畫面",
|
|
90
92
|
showOutput: "顯示輸出", hideOutput: "隱藏輸出", args: "參數", output: "輸出", noOutput: "沒有輸出。",
|
|
91
93
|
showAll: "顯示全部", showLess: "收合", cut: "… 還有 {0} 個字元沒有顯示。", tooLarge: "輸出共 {0},只顯示開頭。",
|
|
92
94
|
times: "×{0}", newer: "{0} 則新的",
|
|
@@ -402,7 +404,11 @@ function Row({ x, prev }) {
|
|
|
402
404
|
};
|
|
403
405
|
let row;
|
|
404
406
|
if (x.kind === "event") {
|
|
405
|
-
|
|
407
|
+
// M-2269: a dialog or prompt AgEnD saw (the CLI never writes it to its transcript): its text as it was on screen,
|
|
408
|
+
// folded until asked for. Text only (a text node), masked by the server.
|
|
409
|
+
row = html`${head(KIND_ICON.event, eventLabel(x.type))}${x.detail ? html`<div class="log-text muted">${x.detail}</div>` : null}
|
|
410
|
+
${x.screen ? html`<button type="button" class="link-btn log-toggle log-screen-toggle" aria-expanded=${open ? "true" : "false"} onClick=${() => setOpen(!open)}>${open ? tl("hideScreen") : tl("showScreen")}</button>` : null}
|
|
411
|
+
${x.screen && open ? html`<div class="log-detail"><div class="log-text mono log-screen">${x.screen}</div></div>` : null}`;
|
|
406
412
|
} else if (x.kind === "user" || x.kind === "assistant") {
|
|
407
413
|
row = html`${head(KIND_ICON[x.kind], tl(x.kind))}${text(x.body, x.bodyCut)}`;
|
|
408
414
|
} else if (x.kind === "error") {
|
|
@@ -78,7 +78,7 @@ register("app", {
|
|
|
78
78
|
statusQueued: "queued", statusStarting: "starting", fleetStarting: "Fleet starting {0}/{1}…", statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
79
79
|
connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
|
|
80
80
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
81
|
-
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
81
|
+
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", reloadPage: "Reload the page", loading: "Loading…",
|
|
82
82
|
noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
|
|
83
83
|
guestChip: "Guest", guestAs: "Guest · {0}", guestAsOn: "Guest · {0} ({1})", guestHint: "You see only the agents you can already use in chat.",
|
|
84
84
|
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device", devicePrefs: "Theme and language",
|
|
@@ -126,7 +126,7 @@ register("app", {
|
|
|
126
126
|
statusQueued: "等待啟動", statusStarting: "啟動中", fleetStarting: "Fleet 啟動中 {0}/{1}…", statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
127
127
|
connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
|
|
128
128
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
129
|
-
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
129
|
+
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", reloadPage: "重新載入頁面", loading: "載入中…",
|
|
130
130
|
noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
|
|
131
131
|
guestChip: "訪客", guestAs: "訪客 · {0}", guestAsOn: "訪客 · {0}({1})", guestHint: "你只會看到在聊天中本來就能用的 agent。",
|
|
132
132
|
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置", devicePrefs: "主題與語言",
|
|
@@ -508,12 +508,14 @@ function BottomTabs({ route, viewOnly }) {
|
|
|
508
508
|
</nav>`;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
function ConnectionLine() {
|
|
511
|
+
export function ConnectionLine() {
|
|
512
512
|
const { connection, hydration, retryHydration } = useStore(appStore);
|
|
513
513
|
// A late catch-up that is not done: the chat may be missing what was open before it loaded (#1425 review).
|
|
514
|
+
// M-2260: never a dead end — beside Try again, a reload reads everything current, whatever kept the catch-up from it.
|
|
514
515
|
if (hydration === "retrying" || hydration === "failed") {
|
|
515
516
|
return html`<div data-trust="status" class="conn" role="status">${hydration === "failed" ? t("app.hydrateFailed") : t("app.hydrating")}
|
|
516
|
-
${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}
|
|
517
|
+
${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}
|
|
518
|
+
${hydration === "failed" ? html` <button type="button" class="btn btn-sm" onClick=${() => location.reload()}>${t("app.reloadPage")}</button>` : null}</div>`;
|
|
517
519
|
}
|
|
518
520
|
if (connection === "reconnecting") return html`<${Reconnecting} />`;
|
|
519
521
|
// #1603: polling is a working transport (the public link polls by design; a buffering proxy makes a page poll), not
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
// - every reconnect catches up once the new stream speaks (catchUp(), with the page's cursor — #1566/#1578), so what
|
|
28
28
|
// was said meanwhile is shown, once.
|
|
29
29
|
// - a 401 is the session ending: nothing is retried (agend-auth.js says so).
|
|
30
|
+
// - M-2260: a catch-up after a long absence carries only the newest messages (the server's budget) and says so
|
|
31
|
+
// (`truncated`): "gap" tells the chats to read their newest history again — it converges like a reload, never by
|
|
32
|
+
// asking a slow link for hours of replies in one answer.
|
|
30
33
|
// - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
|
|
31
34
|
|
|
32
35
|
// #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
|
|
@@ -79,6 +82,22 @@ export function createStream(opts) {
|
|
|
79
82
|
try { return Object.freeze({ source: "sse", receivedAt: now() }); } catch { return undefined; }
|
|
80
83
|
}
|
|
81
84
|
function setConnection(c) { if (state.connection !== c) { state.connection = c; emit("connection", c); } }
|
|
85
|
+
|
|
86
|
+
// M-2260: a tab the browser froze for hours gets every heartbeat it missed at once when it wakes — thousands of status
|
|
87
|
+
// frames, each a whole snapshot, each redrawing the page (seconds of work before the newest message shows). A status
|
|
88
|
+
// that comes after a quiet moment is delivered at once, as before; more statuses before the next turn of the page's
|
|
89
|
+
// task loop replace the waiting one, delivered then — or before any other event, so the order of events never changes.
|
|
90
|
+
// The next turn is a 0 ms timer: in a hidden tab the browser may run it late (a minute when throttled) — a status
|
|
91
|
+
// then waits that long while nobody looks (Needs you, the title's count and notifications ride `needs`, never held).
|
|
92
|
+
let pendingStatus = null, statusBurst = false;
|
|
93
|
+
const turn = () => env.setTimeout(endBurst, 0);
|
|
94
|
+
function endBurst() { statusBurst = false; flushStatus(); }
|
|
95
|
+
function flushStatus() { const d = pendingStatus; pendingStatus = null; if (d !== null) live("status", d, null); }
|
|
96
|
+
function liveStatus(data) {
|
|
97
|
+
if (statusBurst) { pendingStatus = data; return; }
|
|
98
|
+
statusBurst = true; turn();
|
|
99
|
+
live("status", data, null);
|
|
100
|
+
}
|
|
82
101
|
function setHydration(h) { if (state.hydration !== h) { state.hydration = h; emit("hydration", h); } }
|
|
83
102
|
|
|
84
103
|
/** A live event: delivered now, or held (in order) while a catch-up snapshot is on its way. */
|
|
@@ -115,6 +134,7 @@ export function createStream(opts) {
|
|
|
115
134
|
if (polling) { if (!opts.join) pollAgain = true; return polling; }
|
|
116
135
|
const mine = ++pollSeq, ep = readEpoch;
|
|
117
136
|
let status = 0; // this read's HTTP status — its own, never another read's
|
|
137
|
+
flushStatus(); // a status that came before this read is older than its snapshot
|
|
118
138
|
if (sse && !held) held = [];
|
|
119
139
|
let limit = null;
|
|
120
140
|
const read = (async () => {
|
|
@@ -128,6 +148,9 @@ export function createStream(opts) {
|
|
|
128
148
|
if (d.status) emit("status", d.status);
|
|
129
149
|
for (const m of d.messages || []) emit("message", m, { source: "poll" });
|
|
130
150
|
if (typeof d.cursor === "string") state.cursor = d.cursor;
|
|
151
|
+
// M-2260: the server sent only the newest messages of a long absence (its catch-up budget): the chats read their
|
|
152
|
+
// newest history again, as a reload would, instead of this read carrying everything that was missed.
|
|
153
|
+
if (d.truncated === true) emit("gap", {});
|
|
131
154
|
if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
|
|
132
155
|
if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
|
|
133
156
|
if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
|
|
@@ -157,6 +180,7 @@ export function createStream(opts) {
|
|
|
157
180
|
}
|
|
158
181
|
/** #1580 review: the page lets go of everything in flight — reads, the queued poll, the catch-up and its retry wait. */
|
|
159
182
|
function cancelWork() {
|
|
183
|
+
pendingStatus = null; // a page let go of (suspend, end, close) draws no waiting status
|
|
160
184
|
readEpoch++; epoch++;
|
|
161
185
|
pollSeq++; polling = null; pollAgain = false; held = null;
|
|
162
186
|
env.clearTimeout(retryTimer); retryTimer = null;
|
|
@@ -225,6 +249,8 @@ export function createStream(opts) {
|
|
|
225
249
|
else alive();
|
|
226
250
|
// #1823: a clock reading is about now, not about order: it is never held behind a catch-up (it would be stale).
|
|
227
251
|
if (name === "clock") { emit(name, data); return; }
|
|
252
|
+
if (name === "status") { liveStatus(data); return; }
|
|
253
|
+
flushStatus(); // a status waiting goes first: the order never changes
|
|
228
254
|
live(name, data, name === "message" ? e.lastEventId : null, ingress);
|
|
229
255
|
});
|
|
230
256
|
}
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -1872,6 +1872,12 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1872
1872
|
.oo-canvas:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
1873
1873
|
.oo-canvas.hot { cursor: pointer; }
|
|
1874
1874
|
/* The canvas controls, as the org chart canvas's (.oc-zoom, .oc-mini): over the stage's corners. */
|
|
1875
|
+
/* #2946: the key to the items carried between desks — a strip under the bar, as the org chart's (.oc-legend). */
|
|
1876
|
+
.oo-key { font-size: var(--fs-xs); color: var(--text-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 4px 10px; }
|
|
1877
|
+
.oo-key-rows { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); margin: 0; padding: 0; list-style: none; }
|
|
1878
|
+
.oo-key-row { display: inline-flex; align-items: center; gap: 6px; }
|
|
1879
|
+
.oo-key-icon { width: 32px; height: 32px; flex: none; image-rendering: pixelated; }
|
|
1880
|
+
.oo-key-btn.on { border-color: var(--accent); }
|
|
1875
1881
|
.oo-zoom { z-index: 6; }
|
|
1876
1882
|
.oo-zoom .icon-btn { color: var(--text); }
|
|
1877
1883
|
/* The overview's box (left, top, width, height) is set by the page, inside the stage and scaled to it (miniBox). */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Runs `$1` detached from any terminal, at low priority; HUP/INT/TERM on this shell reach its whole process group. */
|
|
2
|
+
export declare const UNATTENDED_INSTALL_SCRIPT: string;
|
|
3
|
+
/** The account's login shell, when it is an absolute path to an executable; else undefined (keep the inherited one). */
|
|
4
|
+
export declare function loginShell(read?: () => string | null | undefined): string | undefined;
|
|
5
|
+
/** The window's command for an install command. `shell`: the $SHELL to give it; null keeps the inherited one. */
|
|
6
|
+
export declare function unattendedInstallCommand(command: string, shell?: string | null): string;
|