@songsid/agend 2.3.4-beta.1 → 2.3.4-beta.11

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.
Files changed (303) hide show
  1. package/THIRD_PARTY_NOTICES.md +37 -0
  2. package/dist/agend-home.d.ts +13 -0
  3. package/dist/agend-home.js +42 -0
  4. package/dist/agend-home.js.map +1 -1
  5. package/dist/agent-cli-instructions.md +1 -1
  6. package/dist/agent-credentials.d.ts +24 -0
  7. package/dist/agent-credentials.js +66 -0
  8. package/dist/agent-credentials.js.map +1 -0
  9. package/dist/agent-endpoint.d.ts +3 -1
  10. package/dist/agent-endpoint.js +13 -0
  11. package/dist/agent-endpoint.js.map +1 -1
  12. package/dist/auth-api.js +10 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/session-authorizations.d.ts +28 -4
  15. package/dist/authority/session-authorizations.js +63 -17
  16. package/dist/authority/session-authorizations.js.map +1 -1
  17. package/dist/backend/antigravity.js +3 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +3 -0
  20. package/dist/backend/claude-code.js +89 -4
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-footer-hints.d.ts +4 -0
  23. package/dist/backend/codex-footer-hints.js +27 -0
  24. package/dist/backend/codex-footer-hints.js.map +1 -0
  25. package/dist/backend/codex-metadata.d.ts +14 -0
  26. package/dist/backend/codex-metadata.js +45 -0
  27. package/dist/backend/codex-metadata.js.map +1 -1
  28. package/dist/backend/codex-session-lookup.d.ts +35 -0
  29. package/dist/backend/codex-session-lookup.js +93 -0
  30. package/dist/backend/codex-session-lookup.js.map +1 -1
  31. package/dist/backend/codex-speed.d.ts +14 -0
  32. package/dist/backend/codex-speed.js +35 -0
  33. package/dist/backend/codex-speed.js.map +1 -0
  34. package/dist/backend/codex.d.ts +27 -0
  35. package/dist/backend/codex.js +133 -25
  36. package/dist/backend/codex.js.map +1 -1
  37. package/dist/backend/kiro-identity.d.ts +2 -0
  38. package/dist/backend/kiro-identity.js +13 -6
  39. package/dist/backend/kiro-identity.js.map +1 -1
  40. package/dist/backend/kiro.d.ts +3 -0
  41. package/dist/backend/kiro.js +68 -5
  42. package/dist/backend/kiro.js.map +1 -1
  43. package/dist/backend/muse.js +5 -3
  44. package/dist/backend/muse.js.map +1 -1
  45. package/dist/backend/types.d.ts +6 -1
  46. package/dist/backend/types.js +3 -0
  47. package/dist/backend/types.js.map +1 -1
  48. package/dist/backend-availability.d.ts +6 -0
  49. package/dist/backend-availability.js +13 -5
  50. package/dist/backend-availability.js.map +1 -1
  51. package/dist/cache-ledger.d.ts +95 -0
  52. package/dist/cache-ledger.js +276 -4
  53. package/dist/cache-ledger.js.map +1 -1
  54. package/dist/cache-service.d.ts +5 -0
  55. package/dist/cache-service.js +16 -4
  56. package/dist/cache-service.js.map +1 -1
  57. package/dist/channel/adapters/discord.js +27 -4
  58. package/dist/channel/adapters/discord.js.map +1 -1
  59. package/dist/channel/adapters/telegram.d.ts +51 -0
  60. package/dist/channel/adapters/telegram.js +238 -18
  61. package/dist/channel/adapters/telegram.js.map +1 -1
  62. package/dist/channel/inbox-temp-name.d.ts +9 -0
  63. package/dist/channel/inbox-temp-name.js +13 -0
  64. package/dist/channel/inbox-temp-name.js.map +1 -0
  65. package/dist/channel/mcp-tools.js +3 -3
  66. package/dist/channel/mcp-tools.js.map +1 -1
  67. package/dist/channel/types.d.ts +2 -0
  68. package/dist/chat-log-ids.d.ts +48 -0
  69. package/dist/chat-log-ids.js +182 -0
  70. package/dist/chat-log-ids.js.map +1 -0
  71. package/dist/classic-channel-manager.d.ts +8 -1
  72. package/dist/classic-channel-manager.js +24 -2
  73. package/dist/classic-channel-manager.js.map +1 -1
  74. package/dist/cli.js +4 -6
  75. package/dist/cli.js.map +1 -1
  76. package/dist/config-validator.js +5 -0
  77. package/dist/config-validator.js.map +1 -1
  78. package/dist/cross-instance-notice.d.ts +10 -0
  79. package/dist/cross-instance-notice.js +67 -5
  80. package/dist/cross-instance-notice.js.map +1 -1
  81. package/dist/daemon.d.ts +52 -0
  82. package/dist/daemon.js +530 -34
  83. package/dist/daemon.js.map +1 -1
  84. package/dist/delivery-consumed-watch.d.ts +23 -1
  85. package/dist/delivery-consumed-watch.js +30 -3
  86. package/dist/delivery-consumed-watch.js.map +1 -1
  87. package/dist/delivery-reconciliation.d.ts +7 -0
  88. package/dist/delivery-reconciliation.js +9 -1
  89. package/dist/delivery-reconciliation.js.map +1 -1
  90. package/dist/dialog-screen-text.d.ts +16 -0
  91. package/dist/dialog-screen-text.js +51 -0
  92. package/dist/dialog-screen-text.js.map +1 -0
  93. package/dist/export-import.d.ts +7 -0
  94. package/dist/export-import.js +44 -10
  95. package/dist/export-import.js.map +1 -1
  96. package/dist/fleet-context.d.ts +2 -0
  97. package/dist/fleet-manager.d.ts +147 -11
  98. package/dist/fleet-manager.js +747 -118
  99. package/dist/fleet-manager.js.map +1 -1
  100. package/dist/fleet-yaml-patch.d.ts +3 -0
  101. package/dist/fleet-yaml-patch.js +103 -0
  102. package/dist/fleet-yaml-patch.js.map +1 -0
  103. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  104. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  105. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  106. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  107. package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
  108. package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -4
  110. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  111. package/dist/install-stream.d.ts +53 -0
  112. package/dist/install-stream.js +134 -0
  113. package/dist/install-stream.js.map +1 -0
  114. package/dist/instance-config-impact.js +2 -0
  115. package/dist/instance-config-impact.js.map +1 -1
  116. package/dist/instance-lifecycle.d.ts +20 -2
  117. package/dist/instance-lifecycle.js +80 -12
  118. package/dist/instance-lifecycle.js.map +1 -1
  119. package/dist/instance-topic-delete.d.ts +77 -0
  120. package/dist/instance-topic-delete.js +123 -0
  121. package/dist/instance-topic-delete.js.map +1 -0
  122. package/dist/instructions.d.ts +1 -1
  123. package/dist/instructions.js +1 -1
  124. package/dist/instructions.js.map +1 -1
  125. package/dist/kiro-engine-status.d.ts +11 -0
  126. package/dist/kiro-engine-status.js +6 -0
  127. package/dist/kiro-engine-status.js.map +1 -1
  128. package/dist/kiro-resume-summary.d.ts +37 -0
  129. package/dist/kiro-resume-summary.js +85 -0
  130. package/dist/kiro-resume-summary.js.map +1 -0
  131. package/dist/locale.js +90 -6
  132. package/dist/locale.js.map +1 -1
  133. package/dist/log-service.d.ts +30 -0
  134. package/dist/log-service.js +36 -3
  135. package/dist/log-service.js.map +1 -1
  136. package/dist/log-steps.d.ts +4 -3
  137. package/dist/log-steps.js +32 -7
  138. package/dist/log-steps.js.map +1 -1
  139. package/dist/logger.d.ts +12 -0
  140. package/dist/logger.js +43 -1
  141. package/dist/logger.js.map +1 -1
  142. package/dist/login-controller.js +9 -1
  143. package/dist/login-controller.js.map +1 -1
  144. package/dist/login-flows.d.ts +2 -0
  145. package/dist/login-flows.js +5 -2
  146. package/dist/login-flows.js.map +1 -1
  147. package/dist/login-manager.d.ts +6 -0
  148. package/dist/login-manager.js +10 -1
  149. package/dist/login-manager.js.map +1 -1
  150. package/dist/missed-offline.d.ts +25 -0
  151. package/dist/missed-offline.js +79 -0
  152. package/dist/missed-offline.js.map +1 -0
  153. package/dist/needs-you-hub.d.ts +6 -1
  154. package/dist/needs-you-hub.js +31 -8
  155. package/dist/needs-you-hub.js.map +1 -1
  156. package/dist/needs-you.d.ts +22 -2
  157. package/dist/needs-you.js +17 -3
  158. package/dist/needs-you.js.map +1 -1
  159. package/dist/office-share-page.d.ts +1 -1
  160. package/dist/office-share-page.js +6 -2
  161. package/dist/office-share-page.js.map +1 -1
  162. package/dist/outbound-handlers.js +59 -3
  163. package/dist/outbound-handlers.js.map +1 -1
  164. package/dist/outbound-schemas.d.ts +1 -0
  165. package/dist/outbound-schemas.js +9 -3
  166. package/dist/outbound-schemas.js.map +1 -1
  167. package/dist/package-preimage.js +27 -2
  168. package/dist/package-preimage.js.map +1 -1
  169. package/dist/queued-handoffs.d.ts +33 -0
  170. package/dist/queued-handoffs.js +75 -0
  171. package/dist/queued-handoffs.js.map +1 -0
  172. package/dist/reply-buttons-controller.js +10 -0
  173. package/dist/reply-buttons-controller.js.map +1 -1
  174. package/dist/scheduler/db.d.ts +2 -1
  175. package/dist/scheduler/db.js +23 -9
  176. package/dist/scheduler/db.js.map +1 -1
  177. package/dist/selection-card.d.ts +2 -3
  178. package/dist/selection-card.js +99 -26
  179. package/dist/selection-card.js.map +1 -1
  180. package/dist/settings-api.d.ts +13 -0
  181. package/dist/settings-api.js +22 -1
  182. package/dist/settings-api.js.map +1 -1
  183. package/dist/settings-change.js +16 -1
  184. package/dist/settings-change.js.map +1 -1
  185. package/dist/settings-instance-patch.js +27 -0
  186. package/dist/settings-instance-patch.js.map +1 -1
  187. package/dist/stall-reporter.d.ts +21 -0
  188. package/dist/stall-reporter.js +82 -0
  189. package/dist/stall-reporter.js.map +1 -0
  190. package/dist/tmux-control.d.ts +2 -2
  191. package/dist/tmux-control.js +4 -2
  192. package/dist/tmux-control.js.map +1 -1
  193. package/dist/tmux-exec-helper.d.ts +6 -3
  194. package/dist/tmux-exec-helper.js +115 -53
  195. package/dist/tmux-exec-helper.js.map +1 -1
  196. package/dist/tmux-manager.d.ts +6 -0
  197. package/dist/tmux-manager.js +17 -0
  198. package/dist/tmux-manager.js.map +1 -1
  199. package/dist/tool-permissions.d.ts +5 -2
  200. package/dist/tool-permissions.js +6 -5
  201. package/dist/tool-permissions.js.map +1 -1
  202. package/dist/tool-progress.d.ts +2 -0
  203. package/dist/tool-progress.js +19 -3
  204. package/dist/tool-progress.js.map +1 -1
  205. package/dist/topic-commands.js +4 -0
  206. package/dist/topic-commands.js.map +1 -1
  207. package/dist/tunnel/cloudflared-install.d.ts +4 -0
  208. package/dist/tunnel/cloudflared-install.js +20 -1
  209. package/dist/tunnel/cloudflared-install.js.map +1 -1
  210. package/dist/tunnel/cloudflared.d.ts +8 -1
  211. package/dist/tunnel/cloudflared.js +34 -6
  212. package/dist/tunnel/cloudflared.js.map +1 -1
  213. package/dist/tunnel/lease.d.ts +3 -3
  214. package/dist/tunnel/lease.js +23 -19
  215. package/dist/tunnel/lease.js.map +1 -1
  216. package/dist/tunnel/manager.js +6 -7
  217. package/dist/tunnel/manager.js.map +1 -1
  218. package/dist/ui/chat-md-strings.js +9 -0
  219. package/dist/ui/chat-store.js +20 -3
  220. package/dist/ui/chat-strings.js +4 -4
  221. package/dist/ui/chat-thread.js +105 -13
  222. package/dist/ui/fleet-canvas.js +4 -3
  223. package/dist/ui/fleet-office-model.js +55 -22
  224. package/dist/ui/fleet-office-overview.js +6 -38
  225. package/dist/ui/fleet-office-pack.js +4 -3
  226. package/dist/ui/fleet-office-skin.js +114 -21
  227. package/dist/ui/fleet-office-store.js +5 -0
  228. package/dist/ui/fleet-office-zones.js +2 -2
  229. package/dist/ui/fleet-office.js +41 -9
  230. package/dist/ui/fleet-usage.js +90 -27
  231. package/dist/ui/panel-chat.js +5 -2
  232. package/dist/ui/panel-log.js +7 -1
  233. package/dist/ui/panel-needs.js +17 -8
  234. package/dist/ui/preview-panel.js +78 -2
  235. package/dist/ui/settings-codex-copy.js +19 -0
  236. package/dist/ui/settings-dialogs.js +8 -0
  237. package/dist/ui/settings-model.js +16 -2
  238. package/dist/ui/settings-strings.js +2 -2
  239. package/dist/ui/shared/app-i18n.js +4 -4
  240. package/dist/ui/shared/app-needs.js +1 -1
  241. package/dist/ui/shared/app-shell.js +4 -2
  242. package/dist/ui/shared/app-stream.js +26 -0
  243. package/dist/ui/shared/app.css +55 -3
  244. package/dist/ui/shared/office/guild.png +0 -0
  245. package/dist/ui/shared/office/school.png +0 -0
  246. package/dist/ui/shared/panel-view.js +339 -17
  247. package/dist/ui/shared/usage-dialog.js +18 -1
  248. package/dist/ui/shared/view-history.css +17 -0
  249. package/dist/ui/shared/view-history.js +438 -0
  250. package/dist/ui/shared/view-strings.js +42 -0
  251. package/dist/unattended-install.d.ts +6 -0
  252. package/dist/unattended-install.js +79 -0
  253. package/dist/unattended-install.js.map +1 -0
  254. package/dist/usage/capacity.d.ts +69 -0
  255. package/dist/usage/capacity.js +119 -0
  256. package/dist/usage/capacity.js.map +1 -0
  257. package/dist/usage/i18n-keys.d.ts +1 -1
  258. package/dist/usage/i18n-keys.js +3 -0
  259. package/dist/usage/i18n-keys.js.map +1 -1
  260. package/dist/usage/profile-timeline.d.ts +16 -0
  261. package/dist/usage/profile-timeline.js +58 -0
  262. package/dist/usage/profile-timeline.js.map +1 -0
  263. package/dist/usage/quota-history.d.ts +43 -0
  264. package/dist/usage/quota-history.js +168 -0
  265. package/dist/usage/quota-history.js.map +1 -0
  266. package/dist/usage/usage-api.d.ts +31 -0
  267. package/dist/usage/usage-api.js +80 -1
  268. package/dist/usage/usage-api.js.map +1 -1
  269. package/dist/usage-report.d.ts +25 -1
  270. package/dist/usage-report.js +55 -4
  271. package/dist/usage-report.js.map +1 -1
  272. package/dist/usage-rollup.d.ts +6 -1
  273. package/dist/usage-rollup.js +16 -4
  274. package/dist/usage-rollup.js.map +1 -1
  275. package/dist/view-api.d.ts +31 -0
  276. package/dist/view-api.js +186 -41
  277. package/dist/view-api.js.map +1 -1
  278. package/dist/view-history.d.ts +84 -0
  279. package/dist/view-history.js +116 -0
  280. package/dist/view-history.js.map +1 -0
  281. package/dist/web-api.d.ts +6 -0
  282. package/dist/web-api.js +15 -4
  283. package/dist/web-api.js.map +1 -1
  284. package/dist/web-chat-history.d.ts +20 -0
  285. package/dist/web-chat-history.js +21 -0
  286. package/dist/web-chat-history.js.map +1 -1
  287. package/dist/web-guest-access.js +5 -1
  288. package/dist/web-guest-access.js.map +1 -1
  289. package/dist/web-login.d.ts +4 -3
  290. package/dist/web-login.js +23 -14
  291. package/dist/web-login.js.map +1 -1
  292. package/dist/web-office-pack-tools.d.ts +1 -1
  293. package/dist/web-office-pack-tools.js +1 -1
  294. package/dist/web-office-pack-tools.js.map +1 -1
  295. package/dist/web-session-authorization.d.ts +13 -2
  296. package/dist/web-session-authorization.js +10 -3
  297. package/dist/web-session-authorization.js.map +1 -1
  298. package/dist/web-session.d.ts +5 -0
  299. package/dist/web-session.js +20 -8
  300. package/dist/web-session.js.map +1 -1
  301. package/launcher/agend-install.cjs +16 -2
  302. package/launcher/private-home.cjs +1 -1
  303. package/package.json +1 -1
@@ -31,14 +31,61 @@ export const TEXT_PREVIEW_COLUMNS = 50;
31
31
  export const TEXT_CARD_MAX = 1024 * 1024;
32
32
  const P = () => globalThis.AgendPreview;
33
33
  /** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
34
- export const STATUS_FOLD_LINES = 6;
35
- export const STATUS_FOLD_CHARS = 600;
34
+ /**
35
+ * #1672 / the user (hmsg-d6ac6dfb): a status line shows two lines — its header ("A → B:") and the first line of the
36
+ * message, each one row (clipped with an ellipsis). Its "open" control shows when that hides something: more lines than
37
+ * two, or a row clipped at the width it is drawn at (measured after each draw and resize: fitStatus). Where nothing is
38
+ * measured (no layout), a line longer than STATUS_LINE_CHARS stands for "clipped".
39
+ */
40
+ export const STATUS_FOLD_LINES = 2;
41
+ export const STATUS_LINE_CHARS = 100;
36
42
  const REPLY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg>';
37
43
  const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
38
44
 
39
45
  /** Escapes & " ' < >: safe for text and inside a quoted attribute. */
40
46
  export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
41
47
  export const msgKey = (x) => `${x.boot}-${x.id}`;
48
+ /** How many elements an HTML string would build: its opening tags (escaped text has none). */
49
+ function tagsIn(html) {
50
+ let n = 0;
51
+ for (let i = html.indexOf("<"); i !== -1; i = html.indexOf("<", i + 1)) { const c = html.charCodeAt(i + 1); if ((c | 32) >= 97 && (c | 32) <= 122) n++; }
52
+ return n;
53
+ }
54
+ /**
55
+ * M-2448: a Markdown file drawn into `el` for the side panel — renderMarkdown, which escapes first (no raw HTML, no
56
+ * script: the same renderer and the same output as the chat's Markdown card), or the text as plain text when its
57
+ * formatting would build more than TEXT_PREVIEW_NODES elements. This module is the one that writes rendered Markdown
58
+ * (web-csp-1268); the panel asks it. Returns "md" or "plain".
59
+ */
60
+ export function drawMarkdown(el, src) {
61
+ const doc = el.ownerDocument;
62
+ el.textContent = "";
63
+ const html = globalThis.AgendChatRender.renderMarkdown(String(src));
64
+ // Each code block gains three elements below (its frame, head and label): counted before anything is built.
65
+ const pres = (html.match(/<pre[\s>]/g) || []).length;
66
+ if (tagsIn(html) + 3 * pres > TEXT_PREVIEW_NODES) {
67
+ const pre = doc.createElement("pre"), code = doc.createElement("code");
68
+ pre.className = "tc-pre"; code.textContent = String(src); pre.append(code); el.append(pre);
69
+ return "plain";
70
+ }
71
+ // Inside the chat's own message container, so the chat's Markdown look applies as it is (lists, links, tables, code)
72
+ // — also with a stylesheet from before this panel existed.
73
+ const msg = doc.createElement("div"), md = doc.createElement("div");
74
+ msg.className = "msg pv-md-msg";
75
+ md.className = "md";
76
+ md.innerHTML = html;
77
+ // Code blocks in the chat's frame, with their language (the thread's Wrap/Copy controls belong to the thread).
78
+ for (const pre of md.querySelectorAll("pre")) {
79
+ const box = doc.createElement("div"), head = doc.createElement("div"), lang = doc.createElement("span");
80
+ box.className = "codeblock"; head.className = "cb-head"; lang.className = "cb-lang";
81
+ lang.textContent = pre.dataset.lang || "text";
82
+ head.append(lang);
83
+ pre.replaceWith(box); box.append(head, pre);
84
+ }
85
+ msg.append(md);
86
+ el.append(msg);
87
+ return "md";
88
+ }
42
89
  /**
43
90
  * Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
44
91
  * at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
@@ -224,15 +271,19 @@ export function createThread(list, scroller, opts) {
224
271
  */
225
272
  function statusHtml(x, time) {
226
273
  const text = String(x.text == null ? "" : x.text);
227
- const lines = R().lineCount(text);
274
+ // Rows split on any line break (a CRLF is one break, never a row and a stray CR); the text itself is not changed.
275
+ const lines = R().lineCount(text.replace(/\r\n?/g, "\n"));
228
276
  const manyLines = lines > STATUS_FOLD_LINES;
229
- const more = manyLines || text.length > STATUS_FOLD_CHARS
230
- ? `<button data-trust="thread-control" type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
231
- : "";
277
+ const overflows = manyLines || text.split(/\r\n?|\n/).some(l => l.length > STATUS_LINE_CHARS);
278
+ // Its control: a short label at the end of line 2; its full name (and whether it is open) for a screen reader. Shown
279
+ // only while the line overflows (class `overflows`, kept by fitStatus at the width it is drawn at).
280
+ const more = `<button data-trust="thread-control" type="button" class="st-more" data-act="toggleFold" aria-expanded="false"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""} aria-label="${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}">${escAttr(manyLines ? trf("chat.showAllShort", lines) : tr("chat.showMoreShort"))}</button>`;
232
281
  const tools = `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
233
282
  // #1990: the other instance's name opens its chat (an in-app link the router takes: no target).
234
- const body = textLinkSegments(text, x.links).map(sg => sg.href ? `<a data-trust="link" class="st-link" href="${escAttr(sg.href)}">${escAttr(sg.text)}</a>` : escAttr(sg.text)).join("");
235
- return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${body}</div>${more}</div>${tools}</div>`;
283
+ // Each line its own row (folded: the first two, each clipped to one row); the line breaks stay in the text.
284
+ const body = textLinkSegments(text, x.links).map(sg => sg.href ? `<a data-trust="link" class="st-link" href="${escAttr(sg.href)}">${escAttr(sg.text)}</a>` : escAttr(sg.text)).join("")
285
+ .split(/\r\n?|\n/).map(l => `<span class="st-ln">${l}</span>`).join("\n");
286
+ return `<div class="msg status-line folded${overflows ? " overflows" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${body}</div>${more}</div>${tools}</div>`;
236
287
  }
237
288
  /**
238
289
  * #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
@@ -355,10 +406,10 @@ export function createThread(list, scroller, opts) {
355
406
  const att = atts.find(a => a && a.id === ph.dataset.att);
356
407
  const type = att ? R().attachmentPreviewType(att) : null;
357
408
  if (!att || !type || type !== ph.dataset.type) continue;
358
- buildTextCard(ph, att, type);
409
+ buildTextCard(ph, att, type, x);
359
410
  }
360
411
  }
361
- function buildTextCard(ph, att, type) {
412
+ function buildTextCard(ph, att, type, x) {
362
413
  const doc = list.ownerDocument;
363
414
  const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
364
415
  ph.textContent = "";
@@ -366,7 +417,16 @@ export function createThread(list, scroller, opts) {
366
417
  const label = el("span", "tc-label", att.name);
367
418
  const show = el("button", "btn btn-sm tc-show", tr("chat.tcShow")); show.type = "button";
368
419
  const dl = el("a", "btn btn-sm tc-dl", tr("chat.tcDownload")); dl.href = `/ui/file/${att.id}`; dl.setAttribute("download", att.name);
369
- head.append(label, show, dl);
420
+ head.append(label, show);
421
+ // M-2448: a Markdown file opens in the side panel too (the panel reads it, or takes what Show already read).
422
+ const panel = opts.panel || null;
423
+ if (type === "md" && panel && typeof panel.openMarkdown === "function" && x) {
424
+ const toPanel = el("button", "btn btn-sm btn-ghost tc-panel", tr("chat.pvOpenPanel")); toPanel.type = "button";
425
+ toPanel.onclick = () => panel.openMarkdown({ key: `${msgKey(x)}:m${att.id}`, instance: x.instance, msgKey: msgKey(x),
426
+ att: { id: att.id, name: att.name, size: att.size }, sender: x.sender, ts: x.ts, ...(text !== null ? { text } : {}) });
427
+ head.append(toPanel);
428
+ }
429
+ head.append(dl);
370
430
  const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
371
431
  const body = el("div", "tc-body"); body.hidden = true;
372
432
  ph.append(head, note, body);
@@ -610,7 +670,25 @@ export function createThread(list, scroller, opts) {
610
670
  }
611
671
  // Sizes change without a scroll event (layout after the render, the dock under the thread): while pinned, the view
612
672
  // stays flush with the bottom; a reader who scrolled up is never moved.
613
- const resized = () => { if (pinned && !atBottom()) toBottom(); reportJump(); };
673
+ const resized = () => { fitStatus(); if (pinned && !atBottom()) toBottom(); reportJump(); };
674
+ /**
675
+ * The user (hmsg-d6ac6dfb): a folded status line whose first two rows fit is plain two lines, no control; one with a
676
+ * row clipped at the width it is drawn at gets its "open" control (`overflows`). Measured after each draw and resize;
677
+ * a line with more than two rows always overflows (data-lines). Nothing laid out (no width): the guess it was drawn with.
678
+ */
679
+ function fitStatus() {
680
+ for (const m of list.querySelectorAll(".msg.status-line.folded")) {
681
+ const btn = m.querySelector(".st-more");
682
+ if (!btn || btn.dataset.lines) continue;
683
+ let measured = false, over = false;
684
+ for (const row of [...m.querySelectorAll(".st-ln")].slice(0, STATUS_FOLD_LINES)) {
685
+ if (!row.clientWidth) continue;
686
+ measured = true;
687
+ if (row.scrollWidth > row.clientWidth + 1) over = true;
688
+ }
689
+ if (measured && m.classList.contains("overflows") !== over) m.classList.toggle("overflows", over);
690
+ }
691
+ }
614
692
  let ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
615
693
  if (ro) { ro.observe(list); ro.observe(scroller); }
616
694
  // #2158: the reader's own hand on the thread — a wheel, a touch, the scrollbar (a pointer held), a scrolling key
@@ -725,6 +803,7 @@ export function createThread(list, scroller, opts) {
725
803
  armPolls();
726
804
  rove();
727
805
  progress();
806
+ fitStatus();
728
807
  return added;
729
808
  }
730
809
  /**
@@ -802,13 +881,26 @@ export function createThread(list, scroller, opts) {
802
881
  function onClick(e) {
803
882
  if (onImageClick(e)) return;
804
883
  const el = e.target && e.target.closest ? e.target.closest("[data-act]") : null;
805
- if (!el || !list.contains(el)) return;
884
+ if (!el || !list.contains(el)) {
885
+ // A folded status line opens by a click on its text too (not on a link in it); its button is the keyboard's way.
886
+ const text = e.target && e.target.closest ? e.target.closest(".msg.status-line.folded.overflows .st-text") : null;
887
+ const btn = text && !e.target.closest("a") ? text.parentNode.querySelector(".st-more") : null;
888
+ if (btn && list.contains(btn)) onClick({ target: btn });
889
+ return;
890
+ }
806
891
  const act = el.dataset.act;
807
892
  if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
808
893
  else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
809
894
  else if (act === "toggleFold") {
810
895
  const box = el.closest(".codeblock, .msg.status-line"); if (!box) return;
811
896
  const folded = box.classList.toggle("folded");
897
+ if (box.classList.contains("status-line")) {
898
+ // A status line's own label: short at the end of line 2 while folded, its full name for a screen reader.
899
+ el.setAttribute("aria-expanded", folded ? "false" : "true");
900
+ el.setAttribute("aria-label", !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore"));
901
+ el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAllShort", el.dataset.lines) : tr("chat.showMoreShort");
902
+ return;
903
+ }
812
904
  el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
813
905
  } else if (act === "toggleWrap") opts.toggleWrap();
814
906
  else if (act === "replyButton") {
@@ -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 !== "list" && live ? html`<${KeyToggle} open=${keyOpen} onToggle=${toggleKey} />` : null}
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 !== "list" && live && keyOpen ? html`<${Legend} />` : null}
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
- export const FLIGHT = { SPEED: 0.35, MIN_MS: 900, MAX_MS: 4000, STAGGER_MS: 150, MAX: 40 };
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 items in the air. `sync(items, now, still)` takes fleet-traffic.js's store items: each new one (a message, a
329
- * delegation's work, its report coming back) starts one trip from its sender's desk to its receiver's; a delegation with
330
- * no report (stale) and anything seen before start none; `still` (reduced motion) starts none. `frame(now, L)` the
331
- * trips under way on layout L: { id, hue, kind, from, x, y, t } (from: who sent it — a pack can draw it as theirs),
332
- * finished ones dropped; a trip whose desks are not drawn goes.
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 createFlights({ max = FLIGHT.MAX } = {}) {
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 k = 0, started = 0;
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
- trips.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now + k++ * FLIGHT.STAGGER_MS, L: null, pts: null, ms: 0 });
346
- started++;
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 (trips.length > max) trips = trips.slice(trips.length - max);
349
- return started;
363
+ if (waiting.length > MAX_WAITING) waiting = waiting.slice(-MAX_WAITING);
364
+ return fresh;
350
365
  },
351
- frame(now, L) {
352
- const out = [];
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.L !== L) { f.L = L; f.pts = walkPath(L, f.from, f.to); f.ms = f.pts ? Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(f.pts) / FLIGHT.SPEED)) : 0; }
355
- if (!f.pts) return false;
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, along, desksOf, membersOf, pathLength } from "./fleet-office-model.js";
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 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, from, x, y,
134
- * t } in overview units; a trip with no crossing (both in one tile, or either gone) is dropped.
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 function createCrossFlights({ max = FLIGHT.MAX } = {}) {
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 refused, but a pack the host
158
- * kept under one before (`kept`) is still read back, listed and deletable, and wins the id for the offices that name it.
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. Three skins are built in. Two draw everything procedurally — little
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. Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
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, x0 = it.x - 8, y0 = it.y - 8 - Math.sin(Math.PI * it.t) * 10;
249
- const b = (c2, r, w, h, col) => px(ctx, x0 + c2 * Q, y0 + r * Q, w * Q, h * Q, col);
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(it.x, it.y + 4, 7, 2.5, 0, 0, Math.PI * 2); ctx.fill();
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
- /** Until its picture is in (loadCozySkin): the studio's floors under the cozy's areas. */
649
- const cozyBare = spriteSkin(COZY_PACK, new Map(), studioSkin);
650
- let cozyJob = null;
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], ["cozy", cozyBare]]);
654
- export const BUILT_IN_SKINS = ["pixel", "studio", "cozy"];
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: the cozy's picture, loaded once; the skin drawn from it then takes the cozy's place in SKINS (skinFor). The
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 loadCozySkin({ image = defaultImage } = {}) {
660
- if (SKINS.get("cozy") !== cozyBare) return Promise.resolve(SKINS.get("cozy"));
661
- cozyJob ??= image(COZY_BACKGROUND).then((im) => {
662
- cozyJob = null;
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(COZY_PACK, new Map([[COZY_BACKGROUND, im]]), studioSkin);
665
- SKINS.set("cozy", sk);
755
+ const sk = spriteSkin(p.pack, new Map([[p.pack.background, im]]), studioSkin);
756
+ SKINS.set(id, sk);
666
757
  return sk;
667
- }, () => { cozyJob = null; return null; });
668
- return cozyJob;
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 { FLIGHT, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
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: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
284
+ ms: flightMs(pathLength(pts)) });
285
285
  started++;
286
286
  }
287
287
  if (letters.length > max) letters = letters.slice(letters.length - max);