@songsid/agend 2.3.1 → 2.3.2-beta.10

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 (338) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/authority-state.d.ts +4 -0
  15. package/dist/authority/authority-state.js +16 -10
  16. package/dist/authority/authority-state.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +7 -1
  18. package/dist/authority/session-authorizations.js +6 -4
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +19 -6
  21. package/dist/authority/web-authority.js +22 -8
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +64 -0
  24. package/dist/backend/kiro-identity.js +101 -0
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro-retired-model.d.ts +62 -0
  27. package/dist/backend/kiro-retired-model.js +160 -0
  28. package/dist/backend/kiro-retired-model.js.map +1 -0
  29. package/dist/backend/kiro.d.ts +71 -2
  30. package/dist/backend/kiro.js +352 -11
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +59 -0
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/backend-signin-guide.d.ts +1 -0
  35. package/dist/backend-signin-guide.js +9 -0
  36. package/dist/backend-signin-guide.js.map +1 -0
  37. package/dist/backend-signin-pending.d.ts +10 -0
  38. package/dist/backend-signin-pending.js +44 -0
  39. package/dist/backend-signin-pending.js.map +1 -0
  40. package/dist/browser-backend-signins.d.ts +56 -0
  41. package/dist/browser-backend-signins.js +189 -0
  42. package/dist/browser-backend-signins.js.map +1 -0
  43. package/dist/cache-ledger.d.ts +38 -2
  44. package/dist/cache-ledger.js +133 -8
  45. package/dist/cache-ledger.js.map +1 -1
  46. package/dist/cache-prices.d.ts +2 -1
  47. package/dist/cache-prices.js +5 -2
  48. package/dist/cache-prices.js.map +1 -1
  49. package/dist/cache-service.d.ts +68 -0
  50. package/dist/cache-service.js +355 -13
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.d.ts +13 -0
  53. package/dist/channel/adapters/discord.js +103 -16
  54. package/dist/channel/adapters/discord.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +34 -7
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.js +2 -1
  58. package/dist/channel/tool-router.js.map +1 -1
  59. package/dist/channel/types.d.ts +11 -0
  60. package/dist/classic-channel-manager.d.ts +11 -0
  61. package/dist/classic-channel-manager.js +39 -1
  62. package/dist/classic-channel-manager.js.map +1 -1
  63. package/dist/cli.js +21 -2
  64. package/dist/cli.js.map +1 -1
  65. package/dist/config-validator.d.ts +7 -1
  66. package/dist/config-validator.js +92 -12
  67. package/dist/config-validator.js.map +1 -1
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.js +31 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/cpu-quota.d.ts +23 -0
  72. package/dist/cpu-quota.js +86 -0
  73. package/dist/cpu-quota.js.map +1 -0
  74. package/dist/daemon.d.ts +57 -3
  75. package/dist/daemon.js +330 -50
  76. package/dist/daemon.js.map +1 -1
  77. package/dist/dashboard-permissions.d.ts +20 -6
  78. package/dist/dashboard-permissions.js +58 -9
  79. package/dist/dashboard-permissions.js.map +1 -1
  80. package/dist/delivery-outbox.d.ts +12 -1
  81. package/dist/delivery-outbox.js +67 -15
  82. package/dist/delivery-outbox.js.map +1 -1
  83. package/dist/doctor.d.ts +3 -0
  84. package/dist/doctor.js +11 -1
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/event-log.d.ts +5 -0
  87. package/dist/event-log.js +13 -0
  88. package/dist/event-log.js.map +1 -1
  89. package/dist/fleet-manager.d.ts +212 -0
  90. package/dist/fleet-manager.js +914 -111
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +1 -0
  93. package/dist/fleet-process-handlers.js +9 -1
  94. package/dist/fleet-process-handlers.js.map +1 -1
  95. package/dist/forged-envelope-scan.d.ts +9 -0
  96. package/dist/forged-envelope-scan.js +30 -5
  97. package/dist/forged-envelope-scan.js.map +1 -1
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  99. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
  102. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
  104. package/dist/guest-seen-people.d.ts +38 -0
  105. package/dist/guest-seen-people.js +78 -0
  106. package/dist/guest-seen-people.js.map +1 -0
  107. package/dist/instance-config-impact.js +14 -6
  108. package/dist/instance-config-impact.js.map +1 -1
  109. package/dist/instance-lifecycle.d.ts +20 -0
  110. package/dist/instance-lifecycle.js +71 -13
  111. package/dist/instance-lifecycle.js.map +1 -1
  112. package/dist/kiro-db-reader.d.ts +5 -0
  113. package/dist/kiro-db-reader.js +18 -0
  114. package/dist/kiro-db-reader.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +2 -0
  116. package/dist/kiro-engine-status.js +9 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-migrate.d.ts +49 -0
  119. package/dist/kiro-migrate.js +105 -0
  120. package/dist/kiro-migrate.js.map +1 -0
  121. package/dist/kiro-transcript-lane.d.ts +2 -0
  122. package/dist/kiro-transcript-lane.js.map +1 -1
  123. package/dist/kiro-transcript-worker.js +1 -0
  124. package/dist/kiro-transcript-worker.js.map +1 -1
  125. package/dist/kiro-tui-forensic.d.ts +7 -0
  126. package/dist/kiro-tui-forensic.js +110 -0
  127. package/dist/kiro-tui-forensic.js.map +1 -0
  128. package/dist/kiro-usage-run.d.ts +7 -0
  129. package/dist/kiro-usage-run.js +39 -0
  130. package/dist/kiro-usage-run.js.map +1 -0
  131. package/dist/kiro-usage-scan.d.ts +49 -0
  132. package/dist/kiro-usage-scan.js +223 -0
  133. package/dist/kiro-usage-scan.js.map +1 -0
  134. package/dist/kiro-usage-worker.d.ts +1 -0
  135. package/dist/kiro-usage-worker.js +10 -0
  136. package/dist/kiro-usage-worker.js.map +1 -0
  137. package/dist/kiro-usage.d.ts +58 -0
  138. package/dist/kiro-usage.js +310 -0
  139. package/dist/kiro-usage.js.map +1 -0
  140. package/dist/locale.js +120 -12
  141. package/dist/locale.js.map +1 -1
  142. package/dist/log-service.js +1 -1
  143. package/dist/log-service.js.map +1 -1
  144. package/dist/login-url.d.ts +4 -0
  145. package/dist/login-url.js +22 -0
  146. package/dist/login-url.js.map +1 -0
  147. package/dist/mac-auto-restore.d.ts +117 -0
  148. package/dist/mac-auto-restore.js +294 -0
  149. package/dist/mac-auto-restore.js.map +1 -0
  150. package/dist/needs-you-hub.d.ts +2 -1
  151. package/dist/needs-you-hub.js +1 -1
  152. package/dist/needs-you-hub.js.map +1 -1
  153. package/dist/needs-you.d.ts +8 -2
  154. package/dist/needs-you.js +6 -0
  155. package/dist/needs-you.js.map +1 -1
  156. package/dist/notice-delivery.d.ts +16 -0
  157. package/dist/notice-delivery.js +35 -0
  158. package/dist/notice-delivery.js.map +1 -0
  159. package/dist/office-share-page.d.ts +16 -0
  160. package/dist/office-share-page.js +85 -0
  161. package/dist/office-share-page.js.map +1 -0
  162. package/dist/office-share-service.d.ts +91 -0
  163. package/dist/office-share-service.js +176 -0
  164. package/dist/office-share-service.js.map +1 -0
  165. package/dist/office-share.d.ts +258 -0
  166. package/dist/office-share.js +549 -0
  167. package/dist/office-share.js.map +1 -0
  168. package/dist/opencode-usage-scan.d.ts +28 -0
  169. package/dist/opencode-usage-scan.js +92 -0
  170. package/dist/opencode-usage-scan.js.map +1 -0
  171. package/dist/opencode-usage-worker.d.ts +1 -0
  172. package/dist/opencode-usage-worker.js +10 -0
  173. package/dist/opencode-usage-worker.js.map +1 -0
  174. package/dist/outbound-handlers.js +22 -2
  175. package/dist/outbound-handlers.js.map +1 -1
  176. package/dist/outbound-schemas.d.ts +28 -2
  177. package/dist/outbound-schemas.js +29 -2
  178. package/dist/outbound-schemas.js.map +1 -1
  179. package/dist/public-web-gateway.js +16 -2
  180. package/dist/public-web-gateway.js.map +1 -1
  181. package/dist/quickstart-api.js +13 -2
  182. package/dist/quickstart-api.js.map +1 -1
  183. package/dist/quickstart.js +2 -1
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/remote-card-answer.d.ts +83 -0
  186. package/dist/remote-card-answer.js +208 -0
  187. package/dist/remote-card-answer.js.map +1 -0
  188. package/dist/reply-triggers.d.ts +2 -0
  189. package/dist/reply-triggers.js +3 -0
  190. package/dist/reply-triggers.js.map +1 -0
  191. package/dist/settings-api.d.ts +28 -0
  192. package/dist/settings-api.js +148 -17
  193. package/dist/settings-api.js.map +1 -1
  194. package/dist/settings-change.js +54 -17
  195. package/dist/settings-change.js.map +1 -1
  196. package/dist/settings-effect.js +12 -3
  197. package/dist/settings-effect.js.map +1 -1
  198. package/dist/settings-instance-patch.js +4 -1
  199. package/dist/settings-instance-patch.js.map +1 -1
  200. package/dist/setup-form.d.ts +1 -1
  201. package/dist/setup-form.js +55 -6
  202. package/dist/setup-form.js.map +1 -1
  203. package/dist/setup-host.d.ts +9 -0
  204. package/dist/setup-host.js +127 -6
  205. package/dist/setup-host.js.map +1 -1
  206. package/dist/setup-signin-assets.d.ts +2 -0
  207. package/dist/setup-signin-assets.js +6 -0
  208. package/dist/setup-signin-assets.js.map +1 -0
  209. package/dist/setup-wizard.js +2 -0
  210. package/dist/setup-wizard.js.map +1 -1
  211. package/dist/start-attempt.d.ts +9 -5
  212. package/dist/start-attempt.js +27 -8
  213. package/dist/start-attempt.js.map +1 -1
  214. package/dist/start-failure-chat.d.ts +4 -0
  215. package/dist/start-failure-chat.js +23 -6
  216. package/dist/start-failure-chat.js.map +1 -1
  217. package/dist/start-failure-notice.d.ts +4 -0
  218. package/dist/start-failure-notice.js +8 -0
  219. package/dist/start-failure-notice.js.map +1 -1
  220. package/dist/start-failure.d.ts +25 -0
  221. package/dist/start-failure.js +75 -2
  222. package/dist/start-failure.js.map +1 -1
  223. package/dist/structured-signin-transport.d.ts +8 -0
  224. package/dist/structured-signin-transport.js +79 -0
  225. package/dist/structured-signin-transport.js.map +1 -0
  226. package/dist/structured-signin.d.ts +131 -0
  227. package/dist/structured-signin.js +388 -0
  228. package/dist/structured-signin.js.map +1 -0
  229. package/dist/tool-permissions.js +12 -0
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.js +6 -2
  232. package/dist/topic-commands.js.map +1 -1
  233. package/dist/transcript-sources.d.ts +26 -2
  234. package/dist/transcript-sources.js +103 -40
  235. package/dist/transcript-sources.js.map +1 -1
  236. package/dist/turn-reply-guard.d.ts +25 -0
  237. package/dist/turn-reply-guard.js +111 -12
  238. package/dist/turn-reply-guard.js.map +1 -1
  239. package/dist/types.d.ts +46 -9
  240. package/dist/ui/backend-signin.js +68 -12
  241. package/dist/ui/chat-ambient.js +1 -1
  242. package/dist/ui/chat-effects.js +4 -4
  243. package/dist/ui/chat-render.js +5 -2
  244. package/dist/ui/chat-strings.js +4 -4
  245. package/dist/ui/chat-thread.js +2 -2
  246. package/dist/ui/first-run.js +2 -2
  247. package/dist/ui/fleet-canvas-view.js +114 -0
  248. package/dist/ui/fleet-canvas.js +15 -77
  249. package/dist/ui/fleet-office-editor.js +94 -14
  250. package/dist/ui/fleet-office-layout.js +235 -0
  251. package/dist/ui/fleet-office-model.js +12 -6
  252. package/dist/ui/fleet-office-overview.js +167 -0
  253. package/dist/ui/fleet-office-pack.d.ts +44 -0
  254. package/dist/ui/fleet-office-pack.js +352 -37
  255. package/dist/ui/fleet-office-skin.js +220 -36
  256. package/dist/ui/fleet-office-store.js +112 -7
  257. package/dist/ui/fleet-office-view.js +155 -0
  258. package/dist/ui/fleet-office-zones.js +264 -42
  259. package/dist/ui/fleet-office.js +957 -160
  260. package/dist/ui/fleet-usage.js +26 -21
  261. package/dist/ui/office-share-dialog.js +145 -0
  262. package/dist/ui/office-share-page.js +164 -0
  263. package/dist/ui/panel-chat.js +9 -2
  264. package/dist/ui/panel-details.js +2 -2
  265. package/dist/ui/panel-fleet.js +11 -77
  266. package/dist/ui/panel-log.js +4 -4
  267. package/dist/ui/panel-needs.js +4 -4
  268. package/dist/ui/panel-settings.js +608 -167
  269. package/dist/ui/search-dialog.js +26 -7
  270. package/dist/ui/settings-appearance.js +82 -29
  271. package/dist/ui/settings-apply.js +42 -7
  272. package/dist/ui/settings-dialogs.js +76 -23
  273. package/dist/ui/settings-draft.js +51 -0
  274. package/dist/ui/settings-guests.js +255 -79
  275. package/dist/ui/settings-learn.js +2 -2
  276. package/dist/ui/settings-model.js +16 -6
  277. package/dist/ui/settings-registry.js +146 -0
  278. package/dist/ui/settings-strings.js +116 -44
  279. package/dist/ui/settings-web-authority.js +34 -23
  280. package/dist/ui/shared/app-i18n.js +18 -2
  281. package/dist/ui/shared/app-nav.js +10 -5
  282. package/dist/ui/shared/app-prefs.js +80 -2
  283. package/dist/ui/shared/app-route.js +39 -5
  284. package/dist/ui/shared/app-shell.js +73 -10
  285. package/dist/ui/shared/app-stream.js +3 -1
  286. package/dist/ui/shared/app.css +307 -17
  287. package/dist/ui/shared/app.js +3 -0
  288. package/dist/ui/shared/effect-stock-trust.js +1 -1
  289. package/dist/ui/shared/identity-color.js +4 -4
  290. package/dist/ui/shared/office/cozy.png +0 -0
  291. package/dist/ui/shared/side-panel.js +95 -0
  292. package/dist/ui/shared/signin.js +2 -2
  293. package/dist/ui/shared/ui-dialog.js +6 -1
  294. package/dist/ui/shared/ui-icons.js +2 -0
  295. package/dist/ui/shared/web-authorize.js +13 -12
  296. package/dist/ui/structured-signin.js +115 -0
  297. package/dist/usage-report.d.ts +20 -0
  298. package/dist/usage-report.js +54 -17
  299. package/dist/usage-report.js.map +1 -1
  300. package/dist/usage-rollup.d.ts +9 -1
  301. package/dist/usage-rollup.js +20 -4
  302. package/dist/usage-rollup.js.map +1 -1
  303. package/dist/web-api.d.ts +26 -2
  304. package/dist/web-api.js +361 -5
  305. package/dist/web-api.js.map +1 -1
  306. package/dist/web-auth.js +8 -2
  307. package/dist/web-auth.js.map +1 -1
  308. package/dist/web-backend-login.d.ts +1 -7
  309. package/dist/web-backend-login.js +2 -24
  310. package/dist/web-backend-login.js.map +1 -1
  311. package/dist/web-experimental.d.ts +1 -1
  312. package/dist/web-experimental.js +1 -1
  313. package/dist/web-experimental.js.map +1 -1
  314. package/dist/web-mod-agent.js +2 -2
  315. package/dist/web-mod-agent.js.map +1 -1
  316. package/dist/web-office-acts.d.ts +173 -0
  317. package/dist/web-office-acts.js +285 -0
  318. package/dist/web-office-acts.js.map +1 -0
  319. package/dist/web-office-skins.d.ts +109 -0
  320. package/dist/web-office-skins.js +299 -0
  321. package/dist/web-office-skins.js.map +1 -0
  322. package/dist/web-office-store.d.ts +8 -2
  323. package/dist/web-office-store.js +77 -14
  324. package/dist/web-office-store.js.map +1 -1
  325. package/dist/web-settings-patch.d.ts +8 -1
  326. package/dist/web-settings-patch.js +65 -5
  327. package/dist/web-settings-patch.js.map +1 -1
  328. package/dist/web-shell-routes.d.ts +4 -2
  329. package/dist/web-shell-routes.js +17 -7
  330. package/dist/web-shell-routes.js.map +1 -1
  331. package/dist/working-recovery.d.ts +104 -0
  332. package/dist/working-recovery.js +298 -0
  333. package/dist/working-recovery.js.map +1 -0
  334. package/dist/working-state-journal.d.ts +5 -1
  335. package/dist/working-state-journal.js +31 -3
  336. package/dist/working-state-journal.js.map +1 -1
  337. package/package.json +3 -2
  338. package/templates/windows-shortcuts.ps1 +155 -140
@@ -15,16 +15,17 @@ register("fusage", {
15
15
  range: "Range", today: "Today", r7d: "7 days", month: "This month", all: "All", custom: "Custom", from: "From", to: "To", apply: "Apply",
16
16
  group: "Group by", g_instance: "Agent", g_backend: "Backend", g_model: "Model", g_profile: "Credential profile", g_team: "Team (as configured now)",
17
17
  csv: "Download CSV", basis: "API-equivalent at list price (checked {0}) — not money spent. Subscriptions are not billed per token.",
18
- amount: "API-equivalent", input: "Input", output: "Output", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
18
+ amount: "API-equivalent", input: "Input", output: "Output", reasoning: "Reasoning", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
19
19
  notPriced: "not priced", partial: "partial", noTeam: "(no team)", unknown: "unknown",
20
- f_cache_write_not_reported: "cache writes not reported (Codex) — an estimate", f_not_priced: "not priced: {0}", f_source_unknown: "read before backend and profile were recorded",
20
+ f_cache_write_not_reported: "cache writes not reported (Codex, Muse) — an estimate", f_not_priced: "not priced: {0}", f_source_unknown: "read before backend and profile were recorded",
21
+ f_estimated: "estimated from text size — Kiro records no token counts", f_includes_estimates: "includes estimates (Kiro)", f_cost_reported: "amount as the CLI reported it (no list price for this model)", estimatedPart: "of which estimated: {0}",
21
22
  scanning: "Scanning — totals may still grow.", updated: "Updated {0} · read every 10 min", since: "Collected since {0}", notYet: "Not collected yet",
22
23
  refresh: "Refresh",
23
24
  notMeasured: "Not measured", nm_credit_billed: "Kiro is credit-billed and records no tokens", nm_unsupported: "AgEnD can't read this CLI's usage yet", nm_no_data: "nothing read yet",
24
25
  shared: "shares its working directory with {0}; their transcripts can't be told apart",
25
- empty: "Nothing recorded yet", emptyHint: "Usage is read from Claude Code and Codex transcripts, from when the fleet started.",
26
- blocked: "Not offered over the public link", blockedHint: "Usage is offered over the public link only when \"Usage report over the public link\" is on in Settings → Web — on the dashboard reached directly, on the computer running AgEnD.",
27
- openSettings: "Open Settings → Web on that computer",
26
+ empty: "Nothing recorded yet", emptyHint: "Usage is read from Claude Code, Codex, Grok, Muse, OpenCode and Antigravity, and estimated for Kiro, from when the fleet started.",
27
+ blocked: "Not offered over the public link", blockedHint: "Usage is offered over the public link only when \"Usage report over the public link\" is on in Settings → Web dashboard — on the dashboard reached directly, on the computer running AgEnD.",
28
+ openSettings: "Open Settings → Web dashboard on that computer",
28
29
  partialDays: "Daily detail from {0}: the earlier days in this range are too many rows to send. The rows and totals above cover the whole range; the trend, the heatmap and the CSV start at {0}.",
29
30
  csvFrom: "Download CSV (from {0})",
30
31
  noDaily: "No daily detail for this range: even its latest day has more rows than one answer carries, and a part of a day is never shown as the day. The rows and totals above are complete; the trend, the heatmap and the CSV need the daily detail and are not shown.",
@@ -42,16 +43,17 @@ register("fusage", {
42
43
  range: "期間", today: "今天", r7d: "7 天", month: "本月", all: "全部", custom: "自訂", from: "從", to: "到", apply: "套用",
43
44
  group: "分組", g_instance: "Agent", g_backend: "Backend", g_model: "模型", g_profile: "憑證 profile", g_team: "Team(依目前設定)",
44
45
  csv: "下載 CSV", basis: "依牌價換算的 API 等值(查證日期 {0})——不是實際花費。訂閱方案不按 token 計費。",
45
- amount: "API 等值", input: "輸入", output: "輸出", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
46
+ amount: "API 等值", input: "輸入", output: "輸出", reasoning: "推理", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
46
47
  notPriced: "未定價", partial: "部分", noTeam: "(不屬於任何 team)", unknown: "未知",
47
- f_cache_write_not_reported: "未回報快取寫入(Codex)——是估算", f_not_priced: "未定價:{0}", f_source_unknown: "讀取時尚未記錄 backend 與 profile",
48
+ f_cache_write_not_reported: "未回報快取寫入(Codex、Muse)——是估算", f_not_priced: "未定價:{0}", f_source_unknown: "讀取時尚未記錄 backend 與 profile",
49
+ f_estimated: "依文字長度估算——Kiro 不記錄 token 數", f_includes_estimates: "含估算值(Kiro)", f_cost_reported: "金額為 CLI 自行回報(價目表沒有此模型)", estimatedPart: "其中估算:{0}",
48
50
  scanning: "掃描中——總數可能還會增加。", updated: "更新於 {0} · 每 10 分鐘讀取一次", since: "自 {0} 起收集", notYet: "尚未收集",
49
51
  refresh: "重新整理",
50
52
  notMeasured: "未量測", nm_credit_billed: "Kiro 以 credit 計費,不記錄 token", nm_unsupported: "AgEnD 目前還讀不到這個 CLI 的用量", nm_no_data: "尚未讀到任何資料",
51
53
  shared: "與 {0} 共用工作目錄,無法分辨各自的 transcript",
52
- empty: "還沒有任何紀錄", emptyHint: "用量從 Claude Code 與 Codex 的 transcript 讀取,自 fleet 啟動起計。",
53
- blocked: "公開連結上不提供", blockedHint: "只有在設定 → 網頁裡開啟「透過公開連結提供用量報告」時,公開連結才會提供用量;這個設定要在執行 AgEnD 的那台電腦上、直接開啟的儀表板裡變更。",
54
- openSettings: "在那台電腦上開啟設定 → 網頁",
54
+ empty: "還沒有任何紀錄", emptyHint: "用量從 Claude Code、Codex、Grok、Muse、OpenCode 與 Antigravity 讀取,Kiro 則為估算,自 fleet 啟動起計。",
55
+ blocked: "公開連結上不提供", blockedHint: "只有在設定 → 網頁儀表板裡開啟「透過公開連結提供用量報告」時,公開連結才會提供用量;這個設定要在執行 AgEnD 的那台電腦上、直接開啟的儀表板裡變更。",
56
+ openSettings: "在那台電腦上開啟設定 → 網頁儀表板",
55
57
  partialDays: "每日明細從 {0} 起:這段期間更早的日子資料列太多,無法一次送出。上方的各列與總計涵蓋整段期間;趨勢、熱度圖與 CSV 則從 {0} 開始。",
56
58
  csvFrom: "下載 CSV(從 {0} 起)",
57
59
  noDaily: "這段期間沒有每日明細:連最近的一天,資料列都超過一次回答能帶的數量,而不完整的一天絕不會被當成那一天顯示。上方的各列與總計是完整的;趨勢、熱度圖與 CSV 需要每日明細,所以不顯示。",
@@ -95,11 +97,11 @@ export function keyText(group, key) {
95
97
  const writes = (r) => (r.cacheWrite5m || 0) + (r.cacheWrite1h || 0) + (r.cacheWrite || 0);
96
98
  function flagText(f, r) {
97
99
  if (f === "not_priced") return tu("f_not_priced", (r.unpriced || []).join(", "));
98
- return ["cache_write_not_reported", "source_unknown"].includes(f) ? tu(`f_${f}`) : f;
100
+ return ["cache_write_not_reported", "source_unknown", "estimated", "includes_estimates", "cost_reported"].includes(f) ? tu(`f_${f}`) : f;
99
101
  }
100
102
 
101
103
  // ── CSV (design §4): the daily rows of this same answer; messages on their own rows (they are per agent, not per model).
102
- const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags"];
104
+ const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "reasoning", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags", "estimated"];
103
105
  /** One CSV field: quoted when it must be, and never read as a formula by a spreadsheet (=, +, -, @, tab, CR first). */
104
106
  export function csvField(v) {
105
107
  let s = v === null || v === undefined ? "" : String(v);
@@ -109,11 +111,11 @@ export function csvField(v) {
109
111
  export function usageCsv(report) {
110
112
  const lines = [CSV_COLUMNS.join(",")];
111
113
  for (const d of report.daily || []) {
112
- lines.push([d.date, d.zone, d.instance, d.backend, d.model, d.tier, d.profile, d.input, d.output, d.cacheRead, d.cacheWrite5m, d.cacheWrite1h, d.cacheWrite,
113
- d.requests, "", d.amount === null ? "" : d.amount.toFixed(6), report.basis, (d.flags || []).join(" ")].map(csvField).join(","));
114
+ lines.push([d.date, d.zone, d.instance, d.backend, d.model, d.tier, d.profile, d.input, d.output, d.reasoning, d.cacheRead, d.cacheWrite5m, d.cacheWrite1h, d.cacheWrite,
115
+ d.requests, "", d.amount === null ? "" : d.amount.toFixed(6), report.basis, (d.flags || []).join(" "), d.estimated ? "yes" : "no"].map(csvField).join(","));
114
116
  }
115
117
  for (const m of report.messagesDaily || []) {
116
- lines.push([m.date, report.zone, m.instance, "", "", "", "", "", "", "", "", "", "", "", m.count, "", report.basis, ""].map(csvField).join(","));
118
+ lines.push([m.date, report.zone, m.instance, "", "", "", "", "", "", "", "", "", "", "", "", m.count, "", report.basis, "", ""].map(csvField).join(","));
117
119
  }
118
120
  return `${lines.join("\r\n")}\r\n`;
119
121
  }
@@ -151,7 +153,7 @@ export function levels(values) {
151
153
  return (v) => (!(v > 0) ? 0 : v <= cut[0] ? 1 : v <= cut[1] ? 2 : v <= cut[2] ? 3 : 4);
152
154
  }
153
155
 
154
- const tokensOf = (d) => d.input + d.output + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite;
156
+ const tokensOf = (d) => d.input + d.output + (d.reasoning || 0) + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite;
155
157
  /**
156
158
  * Each day of the answer: its tokens (whether it was used at all), the priced part of its amount, whether part of it is
157
159
  * not priced, and its parts by zone (a day folded before the fleet's timezone changed keeps its own zone, §5.3).
@@ -218,7 +220,7 @@ export function thinXLabels(n, slotPx, labelWidthPx) {
218
220
  /** The trend's series: the top `TOP` agents by the measure over the range, then "others"; one value per date. */
219
221
  export function trendSeries(report, dates) {
220
222
  const priced = report.totals && report.totals.amount !== null && report.totals.amount > 0;
221
- const measure = (d) => (priced ? d.amount || 0 : d.input + d.output + d.cacheRead + d.cacheWrite5m + d.cacheWrite1h + d.cacheWrite);
223
+ const measure = (d) => (priced ? d.amount || 0 : tokensOf(d));
222
224
  const byAgent = new Map();
223
225
  for (const d of report.daily || []) byAgent.set(d.instance, (byAgent.get(d.instance) || 0) + measure(d));
224
226
  const ranked = [...byAgent.entries()].filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
@@ -327,6 +329,7 @@ function Row({ r, group, max, measure }) {
327
329
  <td data-label=${tu("amount")} class=${`num${r.amount === null ? " muted" : ""}`}>${amountText(r)}</td>
328
330
  <td data-label=${tu("input")} class="num">${tokens(r.input)}</td>
329
331
  <td data-label=${tu("output")} class="num">${tokens(r.output)}</td>
332
+ <td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? "—" : tokens(r.reasoning || 0)}</td>
330
333
  <td data-label=${tu("read")} class="num">${tokens(r.cacheRead)}</td>
331
334
  <td data-label=${tu("write")} class="num">${r.flags.includes("cache_write_not_reported") && writes(r) === 0 ? "—" : tokens(writes(r))}</td>
332
335
  <td data-label=${tu("total")} class="num">${tokens(r.total)}</td>
@@ -379,13 +382,14 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
379
382
  ${report.scanning.active && (!report.scanning.caughtUp || report.scanning.pendingBytes > 0) ? html` <span class="us-scan" role="status">${tu("scanning")} <button type="button" class="btn btn-sm" onClick=${refresh}>${tu("refresh")}</button></span>` : null}</p>
380
383
  ${empty ? html`<${Empty} icon="chart" title=${tu("empty")} hint=${tu("emptyHint")} />` : html`
381
384
  <dl class="us-totals card">
382
- <div class="ch-metric"><dt>${tu("amount")}</dt><dd>${amountText(report.totals)}</dd></div>
385
+ <div class="ch-metric"><dt>${tu("amount")}</dt><dd>${amountText(report.totals)}${report.totals.estimated
386
+ ? html`<span class="us-est">${report.totals.amountEstimated !== null && report.totals.amountEstimated !== undefined ? tu("estimatedPart", money(report.totals.amountEstimated)) : tu("f_includes_estimates")}</span>` : null}</dd></div>
383
387
  <div class="ch-metric"><dt>${tu("total")}</dt><dd>${tokens(report.totals.total)}</dd></div>
384
388
  <div class="ch-metric"><dt>${tu("requests")}</dt><dd>${n(report.totals.requests)}</dd></div>
385
389
  <div class="ch-metric"><dt>${tu("messages")}</dt><dd>${report.totals.messages === null ? "—" : n(report.totals.messages)}</dd></div>
386
390
  </dl>
387
391
  ${rows.length ? html`<table class="us-table"><thead><tr><th scope="col">${tu(`g_${group}`)}</th><th scope="col" class="num">${tu("amount")}</th>
388
- <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th><th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
392
+ <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th><th scope="col" class="num">${tu("reasoning")}</th><th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
389
393
  <th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th><th scope="col" class="num">${tu("messages")}</th></tr></thead>
390
394
  <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} />`)}</tbody></table>`
391
395
  : html`<p class="note">${tu("noRows")}</p>`}
@@ -417,8 +421,9 @@ export function UsageTab({ lease }) {
417
421
  if (r.status === 403 || r.status === 404) {
418
422
  const body = await r.json().catch(() => null);
419
423
  if (!lease.current() || mine !== seq.current) return; // a newer read (or another page) owns the view
420
- // Over the public link: where it is turned on — Settings → Web on the dashboard reached directly.
421
- setState({ blocked: r.status === 403 ? "blocked" : "off", settings: body && typeof body.settings === "string" && /^http:\/\/localhost:\d+\/settings\/web$/.test(body.settings) ? body.settings : null });
424
+ // Over the public link: where it is turned on — Settings → Web dashboard on the dashboard reached directly (#2347;
425
+ // an older AgEnD names /settings/web, which still leads there).
426
+ setState({ blocked: r.status === 403 ? "blocked" : "off", settings: body && typeof body.settings === "string" && /^http:\/\/localhost:\d+\/settings\/(web|web-dashboard)$/.test(body.settings) ? body.settings : null });
422
427
  return;
423
428
  }
424
429
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
@@ -0,0 +1,145 @@
1
+ // #2636: Office share links on the dashboard — the Office's Share… dialog (make one: which office, how long, whether
2
+ // agents' own lines show; then its URL to copy) and the list of open ones (Settings → Access & security, and under the
3
+ // dialog's form): each with its office, when it ends, whether lines show and who made it, Revoke and Revoke all.
4
+ // GET / POST /ui/office-share, DELETE /ui/office-share/<id|all> (web-api.ts): an admin session on the dashboard reached
5
+ // directly only; the public link never offers these.
6
+ import { html, useEffect, useState } from "/assets/app-html.js";
7
+ import { t, register } from "/assets/app-i18n.js";
8
+ import { Dialog } from "/assets/ui-dialog.js";
9
+ import { toast } from "/assets/ui-toast.js";
10
+
11
+ register("oshare", {
12
+ en: {
13
+ share: "Share…", title: "Share the office", lead: "Anyone with the link can watch this office live — names and states, nothing else — until it ends. No sign-in.",
14
+ office: "Office", allOffices: "All offices", how: "For", text: "Include agents' own status lines", create: "Create link", creating: "Starting the link…",
15
+ made: "Anyone with this link can watch until {0}.", copy: "Copy link", copied: "Link copied", open: "Open links", none: "No share links are open.",
16
+ ends: "until {0}", withText: "with status lines", by: "by {0}", revoke: "Revoke", revokeAll: "Revoke all", close: "Close", failed: "The link could not be made.",
17
+ m15: "15 minutes", m30: "30 minutes", h1: "1 hour", h2: "2 hours", h4: "4 hours", h8: "8 hours", h24: "24 hours",
18
+ e_disabled: "Office share links are off on this fleet (web.office_share.enabled: false).", e_office: "That office no longer exists.",
19
+ e_duration: "That is longer than this fleet allows (web.office_share.max_hours).", e_too_many: "At most 10 share links can be open at once: revoke one first.",
20
+ e_rate: "Cloudflare is limiting new quick tunnels right now. Try again in a few minutes.", e_tunnel: "The share link's tunnel did not start ({0}). Try again later.",
21
+ cardTitle: "Office share links", cardHint: "Read-only links to the live Office view, made from Fleet → Org chart → Office → Share…. Each ends at its time; revoke one to end it now.",
22
+ },
23
+ "zh-TW": {
24
+ share: "分享…", title: "分享辦公室", lead: "拿到連結的人都能即時觀看這間辦公室(只有名稱與狀態),直到連結結束,不需要登入。",
25
+ office: "辦公室", allOffices: "所有辦公室", how: "時間", text: "包含 agent 自己的狀態文字", create: "建立連結", creating: "正在啟動連結…",
26
+ made: "拿到這個連結的人可以觀看到 {0}。", copy: "複製連結", copied: "已複製連結", open: "開啟中的連結", none: "目前沒有開啟中的分享連結。",
27
+ ends: "到 {0} 為止", withText: "含狀態文字", by: "由 {0} 建立", revoke: "撤銷", revokeAll: "全部撤銷", close: "關閉", failed: "無法建立連結。",
28
+ m15: "15 分鐘", m30: "30 分鐘", h1: "1 小時", h2: "2 小時", h4: "4 小時", h8: "8 小時", h24: "24 小時",
29
+ e_disabled: "這個 fleet 已關閉辦公室分享連結(web.office_share.enabled: false)。", e_office: "這間辦公室已不存在。",
30
+ e_duration: "超過這個 fleet 允許的時間(web.office_share.max_hours)。", e_too_many: "同時最多 10 個分享連結:先撤銷一個。",
31
+ e_rate: "Cloudflare 目前限制新的臨時通道,請過幾分鐘再試。", e_tunnel: "分享連結的通道沒有啟動({0}),請稍後再試。",
32
+ cardTitle: "辦公室分享連結", cardHint: "即時辦公室畫面的唯讀連結,可在 Fleet → 組織圖 → 辦公室 → 分享… 建立。每個連結到時間就結束;撤銷可立即結束。",
33
+ },
34
+ });
35
+ const to = (k, ...v) => t(`oshare.${k}`, ...v);
36
+ const DURATIONS = [["m15", 15], ["m30", 30], ["h1", 60], ["h2", 120], ["h4", 240], ["h8", 480], ["h24", 1440]];
37
+ const JSON_HEADERS = { "Content-Type": "application/json" };
38
+ const when = (ms) => { try { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; } };
39
+
40
+ /** What a refusal says (the host's `error` and `reason`). */
41
+ export function refusalText(d) {
42
+ const e = d && d.error;
43
+ if (e === "tunnel") return d.reason === "rate-limited" ? to("e_rate") : to("e_tunnel", d.reason || "unknown");
44
+ return ["disabled", "office", "duration", "too_many"].includes(e) ? to(`e_${e}`) : to("failed");
45
+ }
46
+
47
+ /** The open shares, read (and re-read after a change): { enabled, shares } or null while unread / unavailable. */
48
+ export function useShares(get = globalThis.fetch, rev = 0) {
49
+ const [d, setD] = useState(null);
50
+ useEffect(() => {
51
+ let live = true;
52
+ get("/ui/office-share", { method: "GET", headers: JSON_HEADERS }).then((r) => (r.ok ? r.json() : null)).then((v) => { if (live) setD(v); }, () => { if (live) setD(null); });
53
+ return () => { live = false; };
54
+ }, [rev]);
55
+ return d;
56
+ }
57
+
58
+ /** The open shares with Revoke (and Revoke all); `names`: office id → name. */
59
+ export function ShareList({ shares, names = {}, get = globalThis.fetch, onChange }) {
60
+ const [busy, setBusy] = useState(false);
61
+ const revoke = async (id) => {
62
+ setBusy(true);
63
+ try { await get(`/ui/office-share/${id}`, { method: "DELETE", headers: JSON_HEADERS }); } catch { /* the list is read again */ }
64
+ setBusy(false);
65
+ onChange && onChange();
66
+ };
67
+ if (!shares || !shares.length) return html`<p class="muted os-none">${to("none")}</p>`;
68
+ return html`<div class="os-list">
69
+ <ul class="os-rows">${shares.map((s) => html`<li key=${s.id} class="os-row">
70
+ <span class="os-what"><strong>${s.office === "all" ? to("allOffices") : names[s.office] || s.office_name || s.office}</strong>
71
+ <span class="muted"> · ${to("ends", when(s.expiresAt))}${s.text ? ` · ${to("withText")}` : ""}</span></span>
72
+ <button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => revoke(s.id)}>${to("revoke")}</button>
73
+ </li>`)}</ul>
74
+ ${shares.length > 1 ? html`<button type="button" class="btn btn-sm danger os-all" disabled=${busy} onClick=${() => revoke("all")}>${to("revokeAll")}</button>` : null}
75
+ </div>`;
76
+ }
77
+
78
+ /** Share…: make a link for one office (the one shown, by default) or all of them. `offices`: the stored ones. */
79
+ export function ShareDialog({ offices = [], current = null, get = globalThis.fetch, onClose }) {
80
+ const [office, setOffice] = useState(current && offices.some((o) => o.id === current) ? current : "all");
81
+ const [minutes, setMinutes] = useState(60);
82
+ const [text, setText] = useState(false);
83
+ const [busy, setBusy] = useState(false);
84
+ const [made, setMade] = useState(null);
85
+ const [error, setError] = useState("");
86
+ const [rev, setRev] = useState(0);
87
+ const list = useShares(get, rev);
88
+ const names = Object.fromEntries(offices.map((o) => [o.id, o.name]));
89
+ const create = async () => {
90
+ setBusy(true); setError("");
91
+ try {
92
+ const r = await get("/ui/office-share", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify({ office, minutes, text }) });
93
+ const d = await r.json().catch(() => ({}));
94
+ if (r.ok && d && typeof d.url === "string") setMade(d); else setError(refusalText(d));
95
+ } catch { setError(to("failed")); }
96
+ setBusy(false);
97
+ setRev((n) => n + 1);
98
+ };
99
+ const copy = async () => { try { await navigator.clipboard.writeText(made.url); toast(to("copied")); } catch { /* the link is on screen to copy by hand */ } };
100
+ const actions = made ? html`<button type="button" class="btn" onClick=${onClose}>${to("close")}</button>`
101
+ : html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${to("close")}</button>
102
+ <button type="button" class="btn btn-primary" onClick=${create} disabled=${busy}>${busy ? to("creating") : to("create")}</button>`;
103
+ return html`<${Dialog} title=${to("title")} onClose=${onClose} busy=${busy} actions=${actions} wide=${true}>
104
+ <div class="os">
105
+ <p class="muted">${to("lead")}</p>
106
+ ${made ? html`<div class="os-made">
107
+ <input class="input mono os-url" readonly value=${made.url} aria-label=${to("copy")} onFocus=${(e) => e.target.select && e.target.select()} />
108
+ <button type="button" class="btn btn-sm" onClick=${copy}>${to("copy")}</button>
109
+ <p class="note">${to("made", when(made.share.expiresAt))}</p>
110
+ </div>` : html`<div class="os-form">
111
+ <label class="field"><span>${to("office")}</span>
112
+ <select class="tb-select" value=${office} onChange=${(e) => setOffice(e.target.value)} disabled=${busy}>
113
+ ${offices.length > 1 || !offices.length ? html`<option value="all">${to("allOffices")}</option>` : null}
114
+ ${offices.map((o) => html`<option key=${o.id} value=${o.id}>${o.name}</option>`)}
115
+ </select></label>
116
+ <label class="field"><span>${to("how")}</span>
117
+ <select class="tb-select" value=${String(minutes)} onChange=${(e) => setMinutes(Number(e.target.value))} disabled=${busy}>
118
+ ${DURATIONS.map(([k, m]) => html`<option key=${k} value=${String(m)}>${to(k)}</option>`)}
119
+ </select></label>
120
+ <label class="check"><input type="checkbox" checked=${text} onChange=${(e) => setText(e.target.checked)} disabled=${busy} /> ${to("text")}</label>
121
+ ${error ? html`<p class="err os-err" role="alert">${error}</p>` : null}
122
+ </div>`}
123
+ <h3 class="os-head">${to("open")}</h3>
124
+ <${ShareList} shares=${list && list.shares} names=${names} get=${get} onChange=${() => setRev((n) => n + 1)} />
125
+ </div>
126
+ <//>`;
127
+ }
128
+
129
+ /** Settings → Access & security: the open share links, with Revoke (made from the Office's Share…). */
130
+ export function OfficeSharesCard({ get = globalThis.fetch }) {
131
+ const [rev, setRev] = useState(0);
132
+ const d = useShares(get, rev);
133
+ if (!d) return null; // not kept on this fleet, or not this session's to see
134
+ return html`<section class="card os-card"><h3>${to("cardTitle")}</h3>
135
+ <p class="note">${d.enabled === false ? to("e_disabled") : to("cardHint")}</p>
136
+ <${ShareList} shares=${d.shares} get=${get} onChange=${() => setRev((n) => n + 1)} />
137
+ </section>`;
138
+ }
139
+
140
+ /** The Office toolbar's Share… button (shown to an admin on the dashboard reached directly, when the fleet offers it). */
141
+ export function ShareButton({ offices, current, get, disabled = false }) {
142
+ const [open, setOpen] = useState(false);
143
+ return html`<button type="button" class="btn btn-sm oo-share" disabled=${disabled} onClick=${() => setOpen(true)}>${to("share")}</button>
144
+ ${open ? html`<${ShareDialog} offices=${offices} current=${current} get=${get} onClose=${() => setOpen(false)} />` : null}`;
145
+ }
@@ -0,0 +1,164 @@
1
+ // #2636: an Office share link's page (/o/<token> on the share tunnel, src/office-share.ts) — the live Office view for
2
+ // people outside the fleet, read-only, with no sign-in. The same Office as Fleet → Org chart → Office (fleet-office.js),
3
+ // fed by the share's own reads instead of the app shell's: its status and structure (/o/<t>/state, every POLL_MS while
4
+ // the page is shown), the offices, acts and skin packs (the Office's own reads, sent to the share's addresses by the
5
+ // lease's fetch). Nothing opens a chat, edits or writes; there is no other page. When the share ends (revoked, expired,
6
+ // its office deleted) its reads answer 404 and the page says so and stops.
7
+ import { html, render, useEffect, useMemo, useState } from "/assets/app-html.js";
8
+ import { appStore, useStore } from "/assets/app-store.js";
9
+ import { createLease } from "/assets/app-ctx.js";
10
+ import { t, register } from "/assets/app-i18n.js";
11
+ import { buildOrgView } from "./fleet-org.js";
12
+ import { createTrafficStore, POLL_MS } from "./fleet-traffic.js";
13
+ import { Office } from "./fleet-office.js";
14
+ import { ALL_ID } from "./fleet-office-overview.js";
15
+
16
+ register("share", {
17
+ en: { title: "Office", live: "Live view", ends: "until {0}", readOnly: "Read-only: names and states only.", ended: "This Office link has ended.", loading: "Loading the office…", offline: "Can't reach the office right now. Trying again…" },
18
+ "zh-TW": { title: "辦公室", live: "即時畫面", ends: "到 {0} 為止", readOnly: "唯讀:只顯示名稱與狀態。", ended: "這個辦公室連結已結束。", loading: "正在載入辦公室…", offline: "目前連不上辦公室,正在重試…" },
19
+ });
20
+ const to = (k, ...v) => t(`share.${k}`, ...v);
21
+
22
+ /** The share's own address: `/o/<token>` (the page's path). */
23
+ export function shareBase(pathname) {
24
+ const m = /^\/o\/([A-Za-z0-9_-]{43})\/?$/.exec(pathname || "");
25
+ return m ? `/o/${m[1]}` : null;
26
+ }
27
+
28
+ /** The Office's reads, at the share's addresses (anything else is not the share's to answer: refused here). */
29
+ export function shareUrl(base, path) {
30
+ const p = String(path);
31
+ if (p === "/ui/offices") return `${base}/offices`;
32
+ if (p === "/ui/office-acts") return `${base}/office-acts`;
33
+ if (p === "/ui/office-skins" || p.startsWith("/ui/office-skins/") || p.startsWith("/ui/office-skins?")) return `${base}${p.slice(3)}`;
34
+ return null;
35
+ }
36
+
37
+ /** A lease whose fetch reads the share's addresses (createLease's timers), refusing any other. */
38
+ export function shareLease(base, timers = globalThis) {
39
+ return createLease({
40
+ setTimeout: (...a) => timers.setTimeout(...a), clearTimeout: (h) => timers.clearTimeout(h),
41
+ setInterval: (...a) => timers.setInterval(...a), clearInterval: (h) => timers.clearInterval(h),
42
+ fetch: (path, opts) => {
43
+ const url = shareUrl(base, path);
44
+ return url ? timers.fetch(url, opts) : Promise.resolve(new Response("", { status: 404 }));
45
+ },
46
+ });
47
+ }
48
+
49
+ /** #2662: how many times a picture refused for the share's minute (429) is asked for, in all. */
50
+ export const IMAGE_ATTEMPTS = 4;
51
+ /**
52
+ * #2662: pictures read at once. All offices may name a thousand; asked all together they would open more connections
53
+ * than the share's gateway keeps (32, for everyone watching) — a browser on HTTP/2 to the tunnel does not hold back.
54
+ */
55
+ export const IMAGE_PARALLEL = 6;
56
+ let reading = 0;
57
+ const queued = [];
58
+ async function inTurn(job) {
59
+ while (reading >= IMAGE_PARALLEL) await new Promise((go) => queued.push(go));
60
+ reading++;
61
+ try { return await job(); } finally { reading--; queued.shift()?.(); }
62
+ }
63
+ /**
64
+ * #2662: a skin pack's picture on the share (loadPackSkin's `image`): read with fetch, IMAGE_PARALLEL at a time, so a
65
+ * 429 (the share's minute of pictures, shared by everyone watching) is seen and waited out — its Retry-After (1–60 s),
66
+ * up to IMAGE_ATTEMPTS in all — instead of drawn as missing. The picture, or null (not found, or still refused after the
67
+ * last attempt).
68
+ */
69
+ export async function shareImage(url, { fetch: get = globalThis.fetch, timers = globalThis, decode = blobImage } = {}) {
70
+ for (let attempt = 1; ; attempt++) {
71
+ let r, blob = null;
72
+ try {
73
+ r = await inTurn(async () => {
74
+ const answer = await get(url, { method: "GET" });
75
+ if (answer.ok) blob = await answer.blob();
76
+ return answer;
77
+ });
78
+ } catch { return null; }
79
+ if (r.ok) { try { return await decode(blob); } catch { return null; } }
80
+ if (r.status !== 429 || attempt >= IMAGE_ATTEMPTS) return null;
81
+ const after = Number.parseInt(r.headers?.get?.("Retry-After") ?? "", 10);
82
+ const seconds = Number.isFinite(after) ? Math.min(60, Math.max(1, after)) : 60;
83
+ await new Promise((resolve) => timers.setTimeout(resolve, seconds * 1000));
84
+ }
85
+ }
86
+ /** A PNG's bytes as an image to draw (the page's CSP allows blob: pictures). */
87
+ function blobImage(blob) {
88
+ return new Promise((resolve) => {
89
+ if (typeof Image !== "function") { resolve(null); return; }
90
+ const src = URL.createObjectURL(blob), im = new Image();
91
+ im.onload = () => { URL.revokeObjectURL(src); resolve(im); };
92
+ im.onerror = () => { URL.revokeObjectURL(src); resolve(null); };
93
+ im.src = src;
94
+ });
95
+ }
96
+
97
+ /** The share's /state into the app store's shape (the status frame's, as app-store.js takes it). */
98
+ export function appFromState(d) {
99
+ const instances = Array.isArray(d && d.instances) ? d.instances : [];
100
+ const exec = {}, awaiting = {};
101
+ for (const i of instances) {
102
+ exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
103
+ if (i.state === "awaiting_input") awaiting[i.name] = "";
104
+ }
105
+ return { ready: true, instances, exec, awaiting, needs: Array.isArray(d && d.needs) ? d.needs : [], connection: "polling" };
106
+ }
107
+
108
+ const clock = (ms) => {
109
+ try { return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; }
110
+ };
111
+
112
+ export function SharePage({ base, timers = globalThis, pollMs = POLL_MS }) {
113
+ const lease = useMemo(() => shareLease(base, timers), [base]);
114
+ const traffic = useMemo(createTrafficStore, []);
115
+ const [state, setState] = useState({ phase: "loading" }); // loading | live | ended | offline
116
+ const [items, setItems] = useState([]);
117
+ useEffect(() => {
118
+ let stopped = false;
119
+ const read = async () => {
120
+ try {
121
+ const r = await timers.fetch(`${base}/state`, { method: "GET", headers: { Accept: "application/json" } });
122
+ if (stopped || !lease.current()) return;
123
+ if (r.status === 404) { stopped = true; setState({ phase: "ended" }); return; }
124
+ if (!r.ok) { setState((s) => (s.phase === "live" ? s : { phase: "offline" })); return; }
125
+ const d = await r.json();
126
+ if (stopped || !lease.current()) return;
127
+ appStore.set((s) => ({ ...s, ...appFromState(d) }));
128
+ if (d.traffic && traffic.load(d.traffic)) setItems(traffic.items());
129
+ setState({ phase: "live", org: d.org, office: typeof d.office === "string" ? d.office : null, ends: typeof d.ends === "number" ? d.ends : null });
130
+ } catch { if (!stopped) setState((s) => (s.phase === "live" ? s : { phase: "offline" })); }
131
+ };
132
+ void read();
133
+ const hs = [
134
+ lease.interval(() => { if (!stopped && !globalThis.document?.hidden) void read(); }, pollMs),
135
+ lease.interval(() => { if (traffic.sweep()) setItems(traffic.items()); }, 1000),
136
+ ];
137
+ return () => { stopped = true; for (const h of hs) lease.clear(h); };
138
+ }, [lease]);
139
+ useEffect(() => () => lease.dispose(), [lease]);
140
+ const app = useStore(appStore);
141
+ const view = useMemo(() => (state.phase === "live" ? buildOrgView(state.org, app) : null), [state, app.instances, app.needs]);
142
+ if (state.phase === "ended") return html`<main class="share-page share-ended"><p>${to("ended")}</p></main>`;
143
+ return html`<main class="share-page">
144
+ <header class="share-head">
145
+ <strong>${to("title")}</strong>
146
+ <span class="muted">${to("live")}${state.ends ? ` · ${to("ends", clock(state.ends))}` : ""}</span>
147
+ <span class="muted share-note">${to("readOnly")}</span>
148
+ </header>
149
+ ${state.phase === "offline" ? html`<p class="note share-note" role="status">${to("offline")}</p>` : null}
150
+ ${view ? html`<div class="share-stage"><${Office} view=${view} traffic=${items} lease=${lease} assetUrl=${(p) => shareUrl(base, p) || p}
151
+ assetImage=${(url) => shareImage(url, { fetch: (...a) => timers.fetch(...a), timers })} opening=${state.office === "all" ? ALL_ID : state.office || undefined} /></div>`
152
+ : html`<p class="muted share-loading" role="status">${to("loading")}</p>`}
153
+ </main>`;
154
+ }
155
+
156
+ /** Mount on the page (the module's entry): the share's address from the path, else nothing to show. */
157
+ export function mount(root = globalThis.document?.getElementById("app")) {
158
+ const base = shareBase(globalThis.location?.pathname);
159
+ if (!root || !base) return;
160
+ globalThis.document.body.dataset.webTransport = "poll";
161
+ render(html`<${SharePage} base=${base} />`, root);
162
+ }
163
+
164
+ if (globalThis.document?.getElementById?.("app")?.dataset?.share === "1") mount();
@@ -44,6 +44,11 @@ const writeKey = (k, v) => { try { if (v) localStorage.setItem(k, v); else local
44
44
  export const chatCardPrefs = createStore({ open: readKey(CHAT_CARD_KEY, "open"), rowHidden: readKey(CHAT_CARD_ROW_KEY, "hidden") });
45
45
  export const setChatCardOpen = (open) => { writeKey(CHAT_CARD_KEY, open ? "open" : null); chatCardPrefs.set((s) => ({ ...s, open })); };
46
46
  export const setChatCardRowHidden = (rowHidden) => { writeKey(CHAT_CARD_ROW_KEY, rowHidden ? "hidden" : null); chatCardPrefs.set((s) => ({ ...s, rowHidden })); };
47
+ // Full width (the header's ⋯ menu): this browser's choice, like code wrap. The thread and the composer take the panel's
48
+ // width; a person's bubble keeps its cap (app.css). A phone's column is the screen already: no item there.
49
+ const CHAT_WIDE_KEY = "agend_chat_full_width";
50
+ export const chatWide = createStore({ on: readKey(CHAT_WIDE_KEY, "on") });
51
+ export const setChatWide = (on) => { writeKey(CHAT_WIDE_KEY, on ? "on" : null); chatWide.set({ on }); };
47
52
  function ChatCard({ name, card }) {
48
53
  const prefs = useStore(chatCardPrefs);
49
54
  if (!card || !prefs.open) return null;
@@ -260,6 +265,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
260
265
  const publishCanvasPlayback = useCallback(value => setCanvasPlayback(previous =>
261
266
  !previous && !value.playing || previous?.name === value.name && previous.playing === value.playing ? previous : value), []);
262
267
  const [wrap, setWrap] = useState(codeWrap);
268
+ const wide = useStore(chatWide).on;
263
269
  const view = useRef(null), split = useRef(null), thread = useRef(null), bg = useRef(null), effectSurface = useRef(null);
264
270
  useLayoutEffect(() => { applyTheme(view.current, theme, mode); }, [themeKey, mode]);
265
271
  useLayoutEffect(() => {
@@ -321,6 +327,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
321
327
  ...themeMenuItems(name),
322
328
  // #2117: its ambient — why it is not showing here (with the way to change that), and a person's Clear.
323
329
  ...ambientMenuItems(name, { publicLink: document.body.dataset.publicLink === "1", clear: clearAmbient }),
330
+ narrowScreen() ? null : { key: "fullWidth", checkbox: true, checked: wide, label: t("chat.fullWidth"), onSelect: () => setChatWide(!wide) },
324
331
  cwEnabled && canvasWorkerMenuAvailable(window) ? { key: "effects", label: t("cwControls.title"), onSelect: () => setDialog("effects") } : null,
325
332
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
326
333
  ];
@@ -333,7 +340,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
333
340
  const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
334
341
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
335
342
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
336
- return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
343
+ return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} data-width=${wide ? "full" : undefined} ref=${view}>
337
344
  <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`} notice=${html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onPlayback=${publishCanvasPlayback} onClose=${() => setDialog(null)} active=${fxHere} />`}>${fxHere ? html`<${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} />` : null}<${CardMenu} card=${card} /><${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
338
345
  <${ChatCard} key=${name} name=${name} card=${card} />
339
346
  <${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />
@@ -426,7 +433,7 @@ function threadEntry(name, doc) {
426
433
  else if (reason === "busy") getAppEffects()?.stopTrial();
427
434
  else if (reason === "transport") location.reload();
428
435
  else if (reason === "localAdmin") location.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
429
- else navigate("/settings/appearance");
436
+ else navigate("/settings/look");
430
437
  },
431
438
  canTryEffect: x => getAppEffects()?.canTry(x) === true,
432
439
  startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && e.sink && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
@@ -12,7 +12,7 @@ import { appStore, useStore } from "/assets/app-store.js";
12
12
  import { useLease } from "/assets/app-ctx.js";
13
13
  import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
14
14
  import { navigate } from "/assets/app-nav.js";
15
- import { settingsPath } from "/assets/app-route.js";
15
+ import { sectionPath } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
17
17
  import { modelText } from "/assets/instance-nav.js";
18
18
  import { HeaderTools } from "/assets/header-tools.js";
@@ -94,7 +94,7 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
94
94
  // A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'
95
95
  // Classic rows have (pause / wake) — never the fleet agent's (#1561 review).
96
96
  const classic = !!(d && d.kind === "classic");
97
- const editInSettings = () => { requestAgentSettings(name, classic ? "classic" : "agent"); navigate(settingsPath(classic ? "classic" : undefined)); };
97
+ const editInSettings = () => { requestAgentSettings(name, classic ? "classic" : "agent"); navigate(sectionPath(classic ? "classic" : undefined)); };
98
98
  const st = inst.status;
99
99
  // Until its read is in, the page does not know which kind it is: no actions yet (only Edit is offered once known).
100
100
  const items = !d ? [] : classic ? [