@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
@@ -64,7 +64,7 @@ import { nodeText, stateWords } from "./fleet-org.js";
64
64
  import { hits, needle } from "./fleet-canvas-layout.js";
65
65
  import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
66
66
  officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
67
- import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox, tagFootprint } from "./fleet-office-skin.js";
67
+ import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, PICTURE_SKINS, bubbleBox, pixelSkin, loadBuiltInSkin, loadPackSkin, modeOf, skinFor, tagBox, tagFootprint } from "./fleet-office-skin.js";
68
68
  import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
69
69
  import { actsOk, createOfficeStore } from "./fleet-office-store.js";
70
70
  import { badgeOf, fixedOf } from "./fleet-office-levels.js";
@@ -86,7 +86,7 @@ register("orgo", {
86
86
  people: "People", peopleN: "People ({0})", findPerson: "Find a person", everyone: "Show everyone", nobody: "No one here matches this filter.",
87
87
  unseated: "{0} without a seat", skinUnknown: "Its style “{0}” is not available here: drawn as Desks.", closed: "Closed", desks: "Desks", tapAgain: "Tap again to open the chat",
88
88
  capWorking: "Typing away", capIdle: "Tea break", capPaused: "Asleep", capNeeds: "Calling for you", capAway: "Desk empty", capStarting: "Just arriving", capQueued: "Waiting to start",
89
- styleLabel: "Style", style_pixel: "Desks", style_studio: "Studio", style_cozy: "Cozy", cozyNotice: "Add your skin", area_desks: "Work", area_lounge: "Lounge", area_alert: "Help desk", area_nap: "Nap corner",
89
+ styleLabel: "Style", style_pixel: "Desks", style_studio: "Studio", style_cozy: "Cozy", style_guild: "Guild", style_school: "School", cozyNotice: "Add your skin", area_desks: "Work", area_lounge: "Lounge", area_alert: "Help desk", area_nap: "Nap corner",
90
90
  b_working: "On it.|Typing…|Almost there|In the zone|One more test", b_idle: "Tea time|Ready when you are|Stretching|Anything for me?",
91
91
  b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
92
92
  b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
@@ -96,6 +96,8 @@ register("orgo", {
96
96
  fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
97
97
  fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
98
98
  allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
99
+ key: "Key", keyTitle: "What the items carried between desks are", keyLabel: "Carried between desks",
100
+ keyTask: "Folder: a task", keySend: "Envelope: a message", keyReport: "Paper: a report", keyBroadcast: "Horn: a broadcast",
99
101
  tileLabel: "{0} ({1})", openOffice: "Open the office {0}", lobbyTile: "Lobby tile (in no office)", lobbyEveryone: "Everyone is in an office", overviewNoEdit: "Pick one office to edit offices or a layout",
100
102
  },
101
103
  "zh-TW": {
@@ -104,7 +106,7 @@ register("orgo", {
104
106
  people: "成員", peopleN: "成員({0})", findPerson: "尋找成員", everyone: "顯示全部", nobody: "沒有符合這個篩選的成員。",
105
107
  unseated: "{0} 位沒有座位", skinUnknown: "這裡沒有它的風格「{0}」:以辦公桌顯示。", closed: "休息中", desks: "桌子", tapAgain: "再點一下開啟聊天",
106
108
  capWorking: "埋頭打字中", capIdle: "喝茶休息", capPaused: "睡著了", capNeeds: "在呼叫你", capAway: "空位", capStarting: "剛到座位", capQueued: "等待啟動",
107
- styleLabel: "風格", style_pixel: "辦公桌", style_studio: "工作室", style_cozy: "溫馨", cozyNotice: "換上你的外觀包", area_desks: "工作區", area_lounge: "休息區", area_alert: "求救區", area_nap: "午睡角",
109
+ styleLabel: "風格", style_pixel: "辦公桌", style_studio: "工作室", style_cozy: "溫馨", style_guild: "冒險者公會", style_school: "學校", cozyNotice: "換上你的外觀包", area_desks: "工作區", area_lounge: "休息區", area_alert: "求救區", area_nap: "午睡角",
108
110
  b_working: "處理中|打字中…|快好了|專心中|再測一次", b_idle: "喝茶中|隨時可以開工|伸個懶腰|有事找我嗎?",
109
111
  b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
110
112
  b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
@@ -114,6 +116,8 @@ register("orgo", {
114
116
  fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
115
117
  fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
116
118
  allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
119
+ key: "圖例", keyTitle: "桌子之間傳遞的物品代表什麼", keyLabel: "桌子之間傳遞的物品",
120
+ keyTask: "資料夾:任務", keySend: "信封:訊息", keyReport: "紙張:回報", keyBroadcast: "喇叭:廣播",
117
121
  tileLabel: "{0}({1})", openOffice: "開啟辦公室「{0}」", lobbyTile: "大廳格(不在任何辦公室)", lobbyEveryone: "每個成員都已在某間辦公室", overviewNoEdit: "請先選一間辦公室,再編輯辦公室或配置",
118
122
  },
119
123
  });
@@ -584,6 +588,28 @@ function People({ nodes, pinned, onChange }) {
584
588
  </div>`;
585
589
  }
586
590
 
591
+ /** #2946: the key's rows — each kind's item as the scene draws it (the built-in prop, in the theme's colour), and its words. */
592
+ export const KEY_KINDS = [["task", "keyTask"], ["send", "keySend"], ["report", "keyReport"], ["broadcast", "keyBroadcast"]];
593
+ const KEY_PX = 32;
594
+ /**
595
+ * #2946 (hxhsandy): the key to the items carried between desks — a strip under the Office's bar (never over the scene),
596
+ * opened by its Key toggle. The shapes are drawn by the built-in skin, so they are the ones in the air, at their size.
597
+ */
598
+ export function OfficeKey({ pal }) {
599
+ const paint = (hue) => (cv) => {
600
+ const g = cv && typeof cv.getContext === "function" ? cv.getContext("2d") : null;
601
+ if (!g) return;
602
+ const r = Math.max(1, Math.round(globalThis.devicePixelRatio || 1));
603
+ if (cv.width !== KEY_PX * r) { cv.width = KEY_PX * r; cv.height = KEY_PX * r; }
604
+ g.setTransform(r, 0, 0, r, 0, 0); g.clearRect(0, 0, KEY_PX, KEY_PX);
605
+ try { pixelSkin.drawProp(g, { hue, kind: hue, x: KEY_PX / 2, y: KEY_PX / 2 + 4, t: 0 }, 0, { pal }); } catch { /* left blank */ }
606
+ };
607
+ return html`<div class="oo-key" role="region" aria-label=${to("keyLabel")}>
608
+ <ul class="oo-key-rows">${KEY_KINDS.map(([hue, k]) => html`<li key=${hue} class="oo-key-row" data-kind=${hue}>
609
+ <canvas class="oo-key-icon" width=${KEY_PX} height=${KEY_PX} aria-hidden="true" ref=${paint(hue)}></canvas><span>${to(k)}</span></li>`)}</ul>
610
+ </div>`;
611
+ }
612
+
587
613
  /**
588
614
  * The view: `view` buildOrgView's answer; `traffic` the live items (null: Live is off); `q` the Find box's needle;
589
615
  * `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat; none: a desk opens nothing,
@@ -609,13 +635,14 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
609
635
  const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
610
636
  // Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
611
637
  const [nightOn, setNightOn] = useState(() => store.nightOn());
638
+ const [keyOpen, setKeyOpen] = useState(() => store.keyOpen());
612
639
  const [nightNow, setNightNow] = useState(() => isNight());
613
640
  useEffect(() => { if (!nightOn) return undefined; setNightNow(isNight()); const h = setInterval(() => setNightNow(isNight()), NIGHT.TICK); return () => clearInterval(h); }, [nightOn]);
614
641
  const night = nightOn && nightNow;
615
642
  const [lobbyOn, setLobbyOn] = useState(() => store.lobbyTile()); // #2554: All offices' lobby tile
616
643
  const [packs, setPacks] = useState([]); // the skin packs the host keeps: [{ id, name, sha }]
617
644
  const [packSkins, setPackSkins] = useState(() => new Map()); // id@sha → a loaded skin, or null (not a pack)
618
- const [cozyRev, setCozyRev] = useState(0); // #2631: the cozy's picture came in (skinFor has it)
645
+ const [cozyRev, setCozyRev] = useState(0); // #2631/#2947: a built-in's picture came in (skinFor has it)
619
646
  // Edit layout's session while it is open: { id, officeId, skin, pack (the style's identity), base (the pack as kept),
620
647
  // start, draft, rev, list (the offices and rev read when it opened — what Save sends) }; null when closed.
621
648
  const [layout, setLayout] = useState(null);
@@ -801,14 +828,16 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
801
828
  }, [neededKey]);
802
829
  const loading = !!packInfo && !packSkins.has(packKey);
803
830
  const baseSkin = packInfo ? packSkins.get(packKey) || skinFor("pixel") : skinFor(STYLES.includes(wanted) ? wanted : custom ? wanted : "pixel");
804
- // #2631: the cozy's picture, once, when this office (or a tile, or the lobby tile) is drawn in it; until then its bare floors.
805
- const cozyWanted = (overview ? [...stored.map((o) => o.skin), ...(lobbyOn ? [style] : [])] : [wanted]).includes("cozy");
831
+ // #2631/#2947: a picture-backed built-in's picture (cozy, guild, school), once, when this office (or a tile, or the lobby
832
+ // tile) is drawn in it and no kept pack has its id; until then its bare floors.
833
+ const picturesWanted = [...new Set((overview ? [...stored.map((o) => o.skin), ...(lobbyOn ? [style] : [])] : [wanted])
834
+ .filter((id) => PICTURE_SKINS.includes(id) && !packOf(id)))].sort().join(",");
806
835
  useEffect(() => {
807
- if (!cozyWanted) return undefined;
836
+ if (!picturesWanted) return undefined;
808
837
  let live = true;
809
- loadCozySkin().then((sk) => { if (live && sk && sk !== baseSkin) setCozyRev((n) => n + 1); });
838
+ for (const id of picturesWanted.split(",")) loadBuiltInSkin(id).then((sk) => { if (live && sk) setCozyRev((n) => n + 1); });
810
839
  return () => { live = false; };
811
- }, [cozyWanted]);
840
+ }, [picturesWanted]);
812
841
  const zones = !!(baseSkin.pack && baseSkin.pack.stateZones);
813
842
  /** The style's identity: a pack's id and sha (a pack replaced is another), or the built-in skin's id. */
814
843
  const styleKey = packKey || baseSkin.id;
@@ -1456,6 +1485,8 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
1456
1485
  ${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
1457
1486
  ${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
1458
1487
  ${styleErr && auto ? html`<span class="note oo-note oo-style-err" role="status">${styleErr}</span>` : null}
1488
+ ${traffic ? html`<button type="button" class=${`btn btn-sm oo-key-btn${keyOpen ? " on" : ""}`} aria-expanded=${keyOpen ? "true" : "false"} title=${to("keyTitle")}
1489
+ onClick=${() => { const v = !keyOpen; store.saveKeyOpen(v); setKeyOpen(v); }}>${to("key")}</button>` : null}
1459
1490
  ${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
1460
1491
  ${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
1461
1492
  title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
@@ -1463,6 +1494,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
1463
1494
  ${canShare ? html`<${ShareButton} offices=${stored} current=${overview ? "all" : custom ? custom.id : null} disabled=${!!layout}
1464
1495
  get=${lease ? (path, o) => lease.fetch(path, o) : undefined} />` : null}
1465
1496
  </div>
1497
+ ${traffic && keyOpen ? html`<${OfficeKey} pal=${pal} />` : null}
1466
1498
  ${editing && !overview ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
1467
1499
  ask=${ask} current=${() => shown.current} onSaved=${adopt} onClose=${() => setEditing(false)} />` : null}
1468
1500
  ${overview || people.length || L.rooms.length ? null : html`<p class="note oo-empty">${to("nobody")} ${auto && (f.team || f.pinned.length) ? html`<button type="button" class="btn btn-sm" onClick=${() => setF({ team: "", pinned: [] })}>${to("everyone")}</button>` : null}</p>`}
@@ -14,11 +14,20 @@ register("fusage", {
14
14
  en: {
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
- csv: "Download CSV", basis: "API-equivalent at list price (checked {0}) — not money spent. Subscriptions are not billed per token.",
17
+ csv: "Download CSV", basis: "API-equivalent at list price ({0}) — not money spent.", basisMore: "Subscriptions are not billed per token.",
18
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, 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}",
20
+ // #3066 §3: what a row's numbers are, said once each as a small mark with its tooltip (never a pill per row).
21
+ t_not_priced: "Not priced: {0}", t_partial: "Part of this row's models have no price", t_write_unreported: "Codex/Muse don't report cache writes",
22
+ t_cost_reported: "Amount as the CLI reported it", t_estimated: "Estimated from text size (Kiro records no token counts)", t_reasoning_none: "Not reported by this CLI",
23
+ sourceNote: "Some hours were read before AgEnD recorded their backend and profile.", profileUnrecorded: "profile not recorded",
24
+ l_partial: "◐ partly priced", l_cost_reported: "† amount from the CLI", l_estimated: "≈ estimated",
25
+ // #3066 §2: output speed (Claude after the first token, Codex over the response window).
26
+ speed: "Speed", speedUnit: "{0} tok/s", speedCoverage: "{0} of {1} requests timed", speedSpread: "p10–p90: {0}–{1} tok/s",
27
+ speedNone: "no timing in its transcripts", speedByModel: "Speed by model",
28
+ speedBasis: "Output tokens ÷ the model's own response time (tool time excluded); method after tokdash.",
29
+
30
+ f_includes_estimates: "includes estimates (Kiro)", estimatedPart: "of which estimated: {0}",
22
31
  scanning: "Scanning — totals may still grow.", updated: "Updated {0} · read every 10 min", since: "Collected since {0}", notYet: "Not collected yet",
23
32
  refresh: "Refresh",
24
33
  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",
@@ -42,11 +51,18 @@ register("fusage", {
42
51
  "zh-TW": {
43
52
  range: "期間", today: "今天", r7d: "7 天", month: "本月", all: "全部", custom: "自訂", from: "從", to: "到", apply: "套用",
44
53
  group: "分組", g_instance: "Agent", g_backend: "Backend", g_model: "模型", g_profile: "憑證 profile", g_team: "Team(依目前設定)",
45
- csv: "下載 CSV", basis: "依牌價換算的 API 等值(查證日期 {0})——不是實際花費。訂閱方案不按 token 計費。",
54
+ csv: "下載 CSV", basis: "依牌價換算的 API 等值({0})——不是實際花費。", basisMore: "訂閱方案不按 token 計費。",
46
55
  amount: "API 等值", input: "輸入", output: "輸出", reasoning: "推理", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
47
56
  notPriced: "未定價", partial: "部分", noTeam: "(不屬於任何 team)", unknown: "未知",
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}",
57
+ t_not_priced: "未定價:{0}", t_partial: "這一列有部分模型沒有價格", t_write_unreported: "Codex/Muse 不回報快取寫入",
58
+ t_cost_reported: "金額為 CLI 自行回報", t_estimated: "依文字長度估算(Kiro 不記錄 token 數)", t_reasoning_none: "這個 CLI 不回報",
59
+ sourceNote: "部分時段在 AgEnD 記錄 backend 與 profile 之前就已讀取。", profileUnrecorded: "未記錄 profile",
60
+ l_partial: "◐ 部分定價", l_cost_reported: "† 金額由 CLI 回報", l_estimated: "≈ 估算值",
61
+ speed: "速度", speedUnit: "{0} token/秒", speedCoverage: "{1} 個請求中有 {0} 個可計時", speedSpread: "p10–p90:{0}–{1} token/秒",
62
+ speedNone: "transcript 沒有計時資訊", speedByModel: "各模型速度",
63
+ speedBasis: "輸出 token ÷ 模型本身的回應時間(不含工具執行時間);方法參考 tokdash。",
64
+
65
+ f_includes_estimates: "含估算值(Kiro)", estimatedPart: "其中估算:{0}",
50
66
  scanning: "掃描中——總數可能還會增加。", updated: "更新於 {0} · 每 10 分鐘讀取一次", since: "自 {0} 起收集", notYet: "尚未收集",
51
67
  refresh: "重新整理",
52
68
  notMeasured: "未量測", nm_credit_billed: "Kiro 以 credit 計費,不記錄 token", nm_unsupported: "AgEnD 目前還讀不到這個 CLI 的用量", nm_no_data: "尚未讀到任何資料",
@@ -95,10 +111,6 @@ export function keyText(group, key) {
95
111
  return key;
96
112
  }
97
113
  const writes = (r) => (r.cacheWrite5m || 0) + (r.cacheWrite1h || 0) + (r.cacheWrite || 0);
98
- function flagText(f, r) {
99
- if (f === "not_priced") return tu("f_not_priced", (r.unpriced || []).join(", "));
100
- return ["cache_write_not_reported", "source_unknown", "estimated", "includes_estimates", "cost_reported"].includes(f) ? tu(`f_${f}`) : f;
101
- }
102
114
 
103
115
  // ── CSV (design §4): the daily rows of this same answer; messages on their own rows (they are per agent, not per model).
104
116
  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"];
@@ -318,25 +330,73 @@ function Heatmap({ report, dates }) {
318
330
  </section>`;
319
331
  }
320
332
 
321
- function Row({ r, group, max, measure }) {
333
+ /**
334
+ * A mark with what it means: shown as its sign, said (title, and aria-label for readers) as words. role="img" makes it
335
+ * nameable (#3067 review, Prism P3: aria-label is not allowed on a generic span).
336
+ */
337
+ const mark = (sign, words, cls = "us-mark") => html`<span class=${cls} role="img" title=${words} aria-label=${words}>${sign}</span>`;
338
+ /** "—" that says why there is no number (nameable the same way). */
339
+ const dash = (words) => html`<span class="us-none" role="img" title=${words} aria-label=${words}>—</span>`;
340
+ /** The marks a row's numbers carry (#3066 §3): ◐ partly priced, † the CLI's own amount, ≈ estimated. */
341
+ export function rowMarks(r) {
342
+ const flags = r.flags || [];
343
+ return { partial: r.priced === "partial", cost: flags.includes("cost_reported"), est: flags.includes("estimated") || r.estimated === true,
344
+ writeUnreported: flags.includes("cache_write_not_reported") && writes(r) === 0, unpriced: r.amount === null || r.amount === undefined };
345
+ }
346
+ /** A profile as shown: Codex's older hours were never tagged with one. */
347
+ const profileText = (p) => (p === "unrecorded" ? tu("profileUnrecorded") : p);
348
+
349
+ function Row({ r, group, max, measure, cols }) {
322
350
  const share = max > 0 ? Math.max(0.01, measure(r) / max) : 0;
323
- const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.join(", ")].filter(Boolean).join(" · ");
351
+ const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.map(profileText).join(", ")].filter(Boolean).join(" · ");
352
+ const m = rowMarks(r);
353
+ // Estimated rows (Kiro): every number is an estimate — "≈" before each, the reason in its tooltip.
354
+ const num = (text) => (m.est ? html`${mark("≈", tu("t_estimated"), "us-mark us-approx")}${text}` : text);
355
+ const key = group === "profile" ? profileText(keyText(group, r.key)) : keyText(group, r.key);
324
356
  return html`<tr class="us-row">
325
- <th scope="row" class="us-name"><span class="mono">${keyText(group, r.key)}</span>
357
+ <th scope="row" class="us-name"><span class="mono">${key}</span>
326
358
  <svg class="us-bar" aria-hidden="true"><rect class="us-bar-bg" width="100%" height="6" rx="3" /><rect class="us-bar-fg" width=${`${Math.round(share * 100)}%`} height="6" rx="3" /></svg>
327
- ${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}
328
- ${r.flags.length ? html`<span class="us-flags">${r.flags.map((f) => html`<span key=${f} class="ch-tag">${flagText(f, r)}</span>`)}</span>` : null}</th>
329
- <td data-label=${tu("amount")} class=${`num${r.amount === null ? " muted" : ""}`}>${amountText(r)}</td>
330
- <td data-label=${tu("input")} class="num">${tokens(r.input)}</td>
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>
333
- <td data-label=${tu("read")} class="num">${tokens(r.cacheRead)}</td>
334
- <td data-label=${tu("write")} class="num">${r.flags.includes("cache_write_not_reported") && writes(r) === 0 ? "—" : tokens(writes(r))}</td>
335
- <td data-label=${tu("total")} class="num">${tokens(r.total)}</td>
359
+ ${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}</th>
360
+ <td data-label=${tu("amount")} class=${`num${m.unpriced ? " muted" : ""}`}>${m.unpriced ? dash(tu("t_not_priced", (r.unpriced || []).join(", "))) : html`${num(money(r.amount))}${m.partial ? mark("◐", tu("t_partial")) : null}${m.cost ? mark("†", tu("t_cost_reported")) : null}`}</td>
361
+ <td data-label=${tu("input")} class="num">${num(tokens(r.input))}</td>
362
+ <td data-label=${tu("output")} class="num">${num(tokens(r.output))}</td>
363
+ ${cols.reasoning ? html`<td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? dash(tu("t_reasoning_none")) : num(tokens(r.reasoning || 0))}</td>` : null}
364
+ <td data-label=${tu("read")} class="num">${num(tokens(r.cacheRead))}</td>
365
+ <td data-label=${tu("write")} class="num">${m.writeUnreported ? dash(tu("t_write_unreported")) : num(tokens(writes(r)))}</td>
366
+ <td data-label=${tu("total")} class="num">${num(tokens(r.total))}</td>
336
367
  <td data-label=${tu("requests")} class="num">${n(r.requests)}</td>
337
- <td data-label=${tu("messages")} class="num">${r.messages === null ? "—" : n(r.messages)}</td>
368
+ ${cols.messages ? html`<td data-label=${tu("messages")} class="num">${r.messages === null ? "—" : n(r.messages)}</td>` : null}
369
+ ${cols.speed ? html`<td data-label=${tu("speed")} class="num">${r.speed ? speedText(r.speed) : dash(tu("speedNone"))}</td>` : null}
338
370
  </tr>`;
339
371
  }
372
+ /** #3066 §2: a rate as shown — whole tok/s, one decimal below 10. */
373
+ export const speedText = (s) => tu("speedUnit", s.tokPerSec >= 10 ? Math.round(s.tokPerSec).toLocaleString("en-US") : s.tokPerSec.toFixed(1));
374
+ /** Columns worth a place (#3066 §3): Reasoning only when some row reports it; Messages only when some row has them; Speed (§2) only when some response was timed. */
375
+ export function usageColumns(rows) {
376
+ return { reasoning: rows.some((r) => r.reasoningReported !== false), messages: rows.some((r) => r.messages !== null && r.messages !== undefined),
377
+ speed: rows.some((r) => !!r.speed) };
378
+ }
379
+ /** #3066 §2: each model's speed — rate · requests timed (coverage) · p10–p90 from 30 timed. */
380
+ function SpeedCard({ report }) {
381
+ const list = report.speedByModel || [];
382
+ if (!list.length) return null;
383
+ return html`<section class="us-speed card" aria-labelledby="us-speed-h">
384
+ <h3 id="us-speed-h">${tu("speedByModel")}</h3>
385
+ <ul class="rows">${list.map((m) => html`<li key=${m.model} class="row-item"><span class="grow strong mono">${m.model}</span>
386
+ <span class="num">${speedText(m.speed)}</span>
387
+ <span class="muted">${[m.requests > 0 ? tu("speedCoverage", n(m.speed.timedRequests), n(m.requests)) : null,
388
+ m.speed.p10 !== null && m.speed.p90 !== null ? tu("speedSpread", Math.round(m.speed.p10), Math.round(m.speed.p90)) : null].filter(Boolean).join(" · ")}</span></li>`)}</ul>
389
+ <p class="note">${tu("speedBasis")}</p>
390
+ </section>`;
391
+ }
392
+ /** The legend under the table: only the marks that appear, and the one footnote when an hour's source was not recorded. */
393
+ function Legend({ rows }) {
394
+ const marks = rows.map(rowMarks);
395
+ const parts = [marks.some((m) => m.partial) && tu("l_partial"), marks.some((m) => m.cost) && tu("l_cost_reported"), marks.some((m) => m.est) && tu("l_estimated")].filter(Boolean);
396
+ const source = rows.some((r) => (r.flags || []).includes("source_unknown"));
397
+ if (!parts.length && !source) return null;
398
+ return html`<p class="note us-key">${parts.join(" · ")}${parts.length && source ? html`<br />` : null}${source ? tu("sourceNote") : null}</p>`;
399
+ }
340
400
 
341
401
  /** The tab's body for one answer of GET /ui/usage-report. */
342
402
  export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
@@ -355,6 +415,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
355
415
  const zones = zonesChanged(report);
356
416
  const csvFrom = report.dailyTruncated ? report.dailyFrom : report.range.kind === "all" ? "all" : report.range.from;
357
417
  const rows = report.rows.filter((r) => r.total > 0 || (r.messages || 0) > 0);
418
+ const cols = usageColumns(rows);
358
419
  const anyAmount = rows.some((r) => r.amount !== null && r.amount > 0);
359
420
  const measure = (r) => (anyAmount ? r.amount || 0 : r.total);
360
421
  const max = Math.max(0, ...rows.map(measure));
@@ -375,7 +436,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
375
436
  <label>${tu("from")} <input type="date" value=${draft.from} max=${draft.to} onInput=${(e) => setDraft({ ...draft, from: e.target.value })} /></label>
376
437
  <label>${tu("to")} <input type="date" value=${draft.to} min=${draft.from} onInput=${(e) => setDraft({ ...draft, to: e.target.value })} /></label>
377
438
  <button type="submit" class="btn btn-sm">${tu("apply")}</button></form>` : null}
378
- <p class="us-basis" role="note">${tu("basis", report.pricesChecked)}</p>
439
+ <p class="us-basis" role="note" title=${tu("basisMore")}>${tu("basis", report.pricesChecked)}</p>
379
440
  ${noDaily ? html`<p class="us-partial" role="status">${tu("noDaily")}</p>` : report.dailyTruncated ? html`<p class="us-partial" role="status">${tu("partialDays", report.dailyFrom)}</p>` : null}
380
441
  ${zones ? html`<p class="us-partial" role="status">${tu("zoneChanged", zones.join(", "), report.zone)}</p>` : null}
381
442
  <p class="note us-fresh">${tu("updated", new Date(fetchedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))} · ${Number.isFinite(since) ? tu("since", new Date(since).toLocaleDateString()) : tu("notYet")} · ${tu("zoneNote", report.zone)}${group === "team" ? ` ${tu("teamsNote")}` : ""}
@@ -389,10 +450,12 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
389
450
  <div class="ch-metric"><dt>${tu("messages")}</dt><dd>${report.totals.messages === null ? "—" : n(report.totals.messages)}</dd></div>
390
451
  </dl>
391
452
  ${rows.length ? html`<table class="us-table"><thead><tr><th scope="col">${tu(`g_${group}`)}</th><th scope="col" class="num">${tu("amount")}</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>
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>
394
- <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} />`)}</tbody></table>`
453
+ <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th>${cols.reasoning ? html`<th scope="col" class="num">${tu("reasoning")}</th>` : null}<th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
454
+ <th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th>${cols.messages ? html`<th scope="col" class="num">${tu("messages")}</th>` : null}${cols.speed ? html`<th scope="col" class="num">${tu("speed")}</th>` : null}</tr></thead>
455
+ <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} cols=${cols} />`)}</tbody></table>
456
+ <${Legend} rows=${rows} />`
395
457
  : html`<p class="note">${tu("noRows")}</p>`}
458
+ <${SpeedCard} report=${report} />
396
459
  <${Trend} report=${report} dates=${dates} />
397
460
  <${Heatmap} report=${report} dates=${dates} />`}
398
461
  ${report.notMeasured.length || report.shared.length ? html`<section class="us-rest" aria-labelledby="us-rest-h"><h3 id="us-rest-h">${tu("notMeasured")}</h3>
@@ -77,7 +77,7 @@ import { createEmojiIndex } from "./chat-emojis.js";
77
77
  import { pollPublicPreview } from "./preview-status.js";
78
78
  import { createMentionIndex } from "./chat-mentions.js";
79
79
  import { setPanelFeedback } from "./preview-panel-actions.js";
80
- import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
80
+ import { PreviewPanel, claimPanel, openMarkdown, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
81
81
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
82
82
  import { installTour, refreshTourSpot } from "./chat-tour.js";
83
83
  import { LightboxHost, openLightbox } from "./image-lightbox.js";
@@ -438,7 +438,8 @@ function threadEntry(name, doc) {
438
438
  replyButtonBusy: (id) => store.state.rbBusy.has(id),
439
439
  pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
440
440
  serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
441
- panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
441
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims, // #1481, #1704
442
+ openMarkdown: (spec) => openMarkdown(spec) }, // M-2448: a Markdown file in the side panel
442
443
  openImage: openLightbox, // an image, full size, in the page
443
444
  emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
444
445
  mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
@@ -797,6 +798,8 @@ function DeleteDialog({ name, onClose }) {
797
798
  } catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
798
799
  if (r && r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
799
800
  toast(t("chat.instanceDeleted", name));
801
+ // The agent's own topic went too, or stayed with the reason (the server's own words: no new string here).
802
+ if (r.topic_note) toast(r.topic_note, r.topic_deleted !== false);
800
803
  if (handedOver || !lease.current()) return;
801
804
  setBusy(false);
802
805
  onClose();
@@ -49,6 +49,7 @@ register("log", {
49
49
  user: "Asked", assistant: "Replied", error: "Error", errorCode: "Error {0}", turnEnd: "Turn ended", turnEndIn: "Turn ended · {0}",
50
50
  compacted: "Conversation compacted", turn: "Turn",
51
51
  running: "running", done: "done", failed: "failed", interrupted: "interrupted", exit: "exit {0}",
52
+ showScreen: "Show what was on screen", hideScreen: "Hide what was on screen",
52
53
  showOutput: "Show output", hideOutput: "Hide output", args: "Arguments", output: "Output", noOutput: "No output.",
53
54
  showAll: "Show all", showLess: "Show less", cut: "… {0} more characters not shown.", tooLarge: "Output of {0}: only its start is shown.",
54
55
  times: "×{0}", newer: "{0} new",
@@ -87,6 +88,7 @@ register("log", {
87
88
  user: "收到要求", assistant: "回覆", error: "錯誤", errorCode: "錯誤 {0}", turnEnd: "回合結束", turnEndIn: "回合結束 · {0}",
88
89
  compacted: "對話已壓縮", turn: "回合",
89
90
  running: "執行中", done: "完成", failed: "失敗", interrupted: "中斷", exit: "結束碼 {0}",
91
+ showScreen: "顯示當時的畫面", hideScreen: "隱藏當時的畫面",
90
92
  showOutput: "顯示輸出", hideOutput: "隱藏輸出", args: "參數", output: "輸出", noOutput: "沒有輸出。",
91
93
  showAll: "顯示全部", showLess: "收合", cut: "… 還有 {0} 個字元沒有顯示。", tooLarge: "輸出共 {0},只顯示開頭。",
92
94
  times: "×{0}", newer: "{0} 則新的",
@@ -402,7 +404,11 @@ function Row({ x, prev }) {
402
404
  };
403
405
  let row;
404
406
  if (x.kind === "event") {
405
- row = html`${head(KIND_ICON.event, eventLabel(x.type))}${x.detail ? html`<div class="log-text muted">${x.detail}</div>` : null}`;
407
+ // M-2269: a dialog or prompt AgEnD saw (the CLI never writes it to its transcript): its text as it was on screen,
408
+ // folded until asked for. Text only (a text node), masked by the server.
409
+ row = html`${head(KIND_ICON.event, eventLabel(x.type))}${x.detail ? html`<div class="log-text muted">${x.detail}</div>` : null}
410
+ ${x.screen ? html`<button type="button" class="link-btn log-toggle log-screen-toggle" aria-expanded=${open ? "true" : "false"} onClick=${() => setOpen(!open)}>${open ? tl("hideScreen") : tl("showScreen")}</button>` : null}
411
+ ${x.screen && open ? html`<div class="log-detail"><div class="log-text mono log-screen">${x.screen}</div></div>` : null}`;
406
412
  } else if (x.kind === "user" || x.kind === "assistant") {
407
413
  row = html`${head(KIND_ICON[x.kind], tl(x.kind))}${text(x.body, x.bodyCut)}`;
408
414
  } else if (x.kind === "error") {
@@ -25,7 +25,7 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
25
25
  register("needs", {
26
26
  en: {
27
27
  title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
28
- open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
28
+ open: "Open chat", kiroOpen: "Open agent", kiroMigration: "Migration instructions", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
29
29
  gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
30
30
  ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
31
31
  notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
@@ -34,11 +34,11 @@ register("needs", {
34
34
  notifyMobile: "On a phone, rely on your chat app's notification: a new item is a new post in General.", notifyMobileWeb: "On a phone, open this page to see what needs you.",
35
35
  notifyInsecure: "Notifications need a secure address (localhost or the HTTPS public link), not a plain-HTTP LAN address.",
36
36
  notifyUnsupported: "This browser cannot show notifications.", turnOn: "Turn on", turnOff: "Turn off",
37
- typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery",
37
+ typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery", typeMissed: "Missed while offline",
38
38
  },
39
39
  "zh-TW": {
40
40
  title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
41
- open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
41
+ open: "開啟聊天", kiroOpen: "開啟 Agent", kiroMigration: "遷移說明", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
42
42
  gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
43
43
  ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
44
44
  notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
@@ -47,10 +47,18 @@ register("needs", {
47
47
  notifyMobile: "在手機上請以聊天 app 的通知為主:每個新項目都會在 General 發一則新訊息。", notifyMobileWeb: "在手機上請打開這個頁面查看等你處理的事。",
48
48
  notifyInsecure: "通知需要安全的網址(localhost 或 HTTPS 公開連結),區網的一般 HTTP 網址不行。",
49
49
  notifyUnsupported: "這個瀏覽器無法顯示通知。", turnOn: "開啟", turnOff: "關閉",
50
- typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送",
50
+ typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送", typeMissed: "離線時錯過",
51
51
  },
52
52
  });
53
53
  const tn = (k, ...v) => t(`needs.${k}`, ...v);
54
+ /** Stale shells keep the old app-i18n.js; a missing key would otherwise show as app.needs_reason_*. */
55
+ function itemReason(item) {
56
+ const text = reasonText(item);
57
+ if (typeof text === "string" && text.startsWith("app.needs_reason_") && (item.type === "missed" || item.reason === "missed_offline")) {
58
+ return tn("typeMissed");
59
+ }
60
+ return text;
61
+ }
54
62
 
55
63
  /**
56
64
  * The page's claims on what Needs you writes, kept for the life of the page — not of a panel or a render (#1463
@@ -86,7 +94,7 @@ const setClaim = (kind, key, value) => claims.set((c) => {
86
94
  return { ...c, [kind]: next };
87
95
  });
88
96
 
89
- const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks" };
97
+ const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks", missed: "clock" };
90
98
  const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
91
99
 
92
100
  /**
@@ -195,16 +203,17 @@ function NeedsItem({ item, nowMs, owner }) {
195
203
  return html`<article data-trust="permission" class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
196
204
  <span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
197
205
  <div class="n-body">
198
- <div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
206
+ <div class="n-title"><strong>${itemReason(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
199
207
  ${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
200
208
  <div class="n-actions">
201
209
  ${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.filter((a) => !a.input).map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
202
210
  disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
203
- ${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
211
+ ${item.type === "delivery" || item.type === "missed" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
204
212
  <${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
205
213
  ${(item.reason === "auth_paused" || item.reason === "backend_signin") && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
206
214
  ${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
207
- ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>` : null}
215
+ ${item.kiroChoice === true ? html`<a class="btn btn-sm" href="https://github.com/songsid/AgEnD/blob/main/src/general-knowledge/skills/kiro-engine-migration/SKILL.md" target="_blank" rel="noopener noreferrer">${tn("kiroMigration")}</a>` : null}
216
+ ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn(item.kiroChoice === true ? "kiroOpen" : "open")}</a>` : null}
208
217
  </div>
209
218
  </div>
210
219
  </article>`;
@@ -20,6 +20,8 @@ import { chatTime, onDayChange } from "./chat-time.js";
20
20
  import { PanelActions } from "./preview-panel-actions.js";
21
21
  import { htmlTitle, versionsOf, lineDiff, diffHunks, DIFF_LIMITS } from "./preview-versions.js";
22
22
  import { pagesOf, pageIdOf } from "./preview-pages.js";
23
+ import { drawMarkdown, TEXT_CARD_MAX } from "./chat-thread.js";
24
+ import "./chat-md-strings.js"; // M-2448: the Markdown panel's own words (an older cached chat-strings.js lacks them)
23
25
 
24
26
  export { htmlTitle };
25
27
 
@@ -57,6 +59,16 @@ export function openPanel(spec, { run = false } = {}) {
57
59
  set({ open: { ...spec, kind: "html" }, run: run ? ++runs : 0 });
58
60
  retire(old); // State changes first: cleanup reentry cannot close a newer panel.
59
61
  }
62
+ /**
63
+ * M-2448: show a Markdown file in the panel — `spec`: { key, instance, msgKey, att: { id, name, size }, sender, ts,
64
+ * text? } (text: what the card's Show already read). Nothing runs: it is rendered by the chat's own Markdown renderer.
65
+ * The block shown before stops, as for any open.
66
+ */
67
+ export function openMarkdown(spec) {
68
+ const old = state.open;
69
+ set({ open: { ...spec, kind: "md" }, run: 0 });
70
+ retire(old);
71
+ }
60
72
  /** Close publishes the empty slot before old-owner callbacks can reenter. */
61
73
  export function closePanel() {
62
74
  const old = state.open; if (!old) return;
@@ -363,7 +375,35 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
363
375
  }
364
376
  return () => { live = false; };
365
377
  }, [shown, open && open.key, prev && prev.key]);
378
+ // M-2448: a Markdown file's text (read once per open: the same capped, same-origin read as the chat's card) and
379
+ // which view shows it — rendered (the chat's Markdown renderer, which escapes first) or its source.
380
+ const [md, setMd] = useState(null); // { state: "loading" | "ready" | "failed", text?, reason? }
381
+ const [mdView, setMdView] = useState("rendered");
382
+ const [mdPlain, setMdPlain] = useState(false);
383
+ const mdBody = useRef(null);
384
+ const isMd = !!open && open.kind === "md";
385
+ useEffect(() => {
386
+ if (!isMd) { setMd(null); return undefined; }
387
+ setMdView("rendered");
388
+ if (typeof open.text === "string") { setMd({ state: "ready", text: open.text }); return undefined; }
389
+ let live = true;
390
+ setMd({ state: "loading" });
391
+ loadHtmlAttachment(open.att, undefined, { ask: TEXT_CARD_MAX, max: TEXT_CARD_MAX }).then((r) => {
392
+ if (!live) return;
393
+ setMd(r.ok ? { state: "ready", text: r.code } : { state: "failed", reason: r.reason === "over" ? t("chat.tcOver") : attReason(r) });
394
+ }, () => { if (live) setMd({ state: "failed", reason: t("chat.pvAttFailed") }); });
395
+ return () => { live = false; };
396
+ }, [isMd && open.key]);
397
+ useEffect(() => {
398
+ if (!isMd || !md || md.state !== "ready" || mdView !== "rendered" || !mdBody.current) return;
399
+ setMdPlain(drawMarkdown(mdBody.current, md.text) === "plain");
400
+ }, [isMd && open.key, md, mdView]);
366
401
  const fileName = open && open.att ? open.att.name : undefined;
402
+ // The divider between the thread and the panel (both kinds): drag, or the arrow keys, Home and End.
403
+ function divider0() {
404
+ return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
405
+ aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>`;
406
+ }
367
407
  // Esc in the panel closes it (and is not the chat's "stop the reply").
368
408
  const onPanelKey = (e) => {
369
409
  if (e.key !== "Escape" || e.isComposing) return;
@@ -372,6 +412,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
372
412
  };
373
413
 
374
414
  if (!open) return null;
415
+ if (isMd) return html`${divider0()}<${MarkdownPanel} open=${open} name=${name} md=${md} view=${mdView} setView=${setMdView} plain=${mdPlain}
416
+ body=${mdBody} panel=${panel} fs=${fs} enterFs=${enterFs} leaveFs=${leaveFs} onKey=${onPanelKey} reveal=${reveal} />`;
375
417
  const going = run.state === "starting" || run.state === "running";
376
418
  const a = P().availability();
377
419
  // #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm). Allowing
@@ -390,8 +432,7 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
390
432
  if (want && pending.current === want && panelStore.get().run === want) start();
391
433
  };
392
434
  const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
393
- return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
394
- aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
435
+ return html`${divider0()}
395
436
  <aside data-trust="preview" class=${`pv-panel${fs ? ` pv-fs pv-fs-${fs}` : ""}`} ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
396
437
  <div class="pv-panel-head">
397
438
  <button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
@@ -440,6 +481,41 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
440
481
  </aside>`;
441
482
  }
442
483
 
484
+ /**
485
+ * M-2448: a Markdown file in the panel — the HTML preview's frame (head, source, close, full screen; a full-screen
486
+ * sheet on a phone), with Rendered / Source and Download. Nothing in the file runs: Rendered is the chat's Markdown
487
+ * renderer (escaped first, no raw HTML), Source is text.
488
+ */
489
+ function MarkdownPanel({ open, name, md, view, setView, plain, body, panel, fs, enterFs, leaveFs, onKey, reveal }) {
490
+ const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
491
+ const ready = md && md.state === "ready";
492
+ return html`<aside data-trust="preview" class=${`pv-panel pv-md-panel${fs ? ` pv-fs pv-fs-${fs}` : ""}`} ref=${panel} aria-label=${t("chat.pvMdPanel")} onKeyDown=${onKey}>
493
+ <div class="pv-panel-head">
494
+ <button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
495
+ <div class="pv-panel-title"><strong>${open.att ? open.att.name : t("chat.pvMdPanel")}</strong>
496
+ <button type="button" class="pv-source" title=${source} onClick=${() => { if (window.matchMedia && window.matchMedia("(max-width: 899px)").matches) closePanel(); reveal(open.msgKey); }}>${source}</button></div>
497
+ <button type="button" class="icon-btn only-wide pv-panel-close" title=${t("chat.pvClose")} aria-label=${t("chat.pvClose")} onClick=${closePanel}><${Icon} name="close" size=${18} /></button>
498
+ </div>
499
+ <div class="pv-panel-bar">
500
+ <div class="seg-inline" role="group" aria-label=${t("chat.pvMdPanel")}>
501
+ <button type="button" class=${`btn btn-sm${view === "rendered" ? " on" : ""}`} aria-pressed=${view === "rendered" ? "true" : "false"} title=${t("chat.pvMdRendered")} onClick=${() => setView("rendered")}><${Icon} name="eye" size=${14} /><span class="lbl">${t("chat.pvMdRendered")}</span></button>
502
+ <button type="button" class=${`btn btn-sm${view === "source" ? " on" : ""}`} aria-pressed=${view === "source" ? "true" : "false"} title=${t("chat.pvMdSource")} onClick=${() => setView("source")}><${Icon} name="code" size=${14} /><span class="lbl">${t("chat.pvMdSource")}</span></button>
503
+ </div>
504
+ <span class="grow"></span>
505
+ <button type="button" class=${`btn btn-sm only-wide pv-panel-fs${fs ? " on" : ""}`} aria-pressed=${fs ? "true" : "false"} title=${t(fs ? "chat.pvFsExit" : "chat.pvFs")} aria-label=${t(fs ? "chat.pvFsExit" : "chat.pvFs")}
506
+ onClick=${() => (fs ? leaveFs() : enterFs())}><${Icon} name=${fs ? "fullscreenexit" : "fullscreen"} size=${14} /></button>
507
+ ${open.att ? html`<a class="btn btn-sm pv-panel-dl" href=${`/ui/file/${open.att.id}`} download=${open.att.name} title=${t("chat.pvDownload")}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></a>` : null}
508
+ </div>
509
+ ${md && md.state === "loading" ? html`<div class="pv-note" role="status">${t("chat.pvLoading")}</div>` : null}
510
+ ${md && md.state === "failed" ? html`<div class="pv-note" role="alert">${md.reason}</div>` : null}
511
+ ${ready && view === "rendered" && plain ? html`<div class="pv-note" role="status">${t("chat.tcPlain")}</div>` : null}
512
+ <div class="pv-panel-body show-code">
513
+ ${ready && view === "rendered" ? html`<div class="pv-code pv-md" ref=${body}></div>` : null}
514
+ ${ready && view === "source" ? html`<pre class="pv-code"><code>${md.text}</code></pre>` : null}
515
+ </div>
516
+ </aside>`;
517
+ }
518
+
443
519
  /** The Changes view: hunks of the line diff as a unified diff shows them, drawn as text (never as HTML). */
444
520
  function Changes({ changes }) {
445
521
  if (!changes || changes.state === "loading") return html`<div class="pv-note pv-diff-note">${t("chat.pvLoading")}</div>`;