@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20

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 (389) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli-instructions.md +6 -2
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +3 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.js +16 -4
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +3 -1
  11. package/dist/auth-api.js +17 -1
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/antigravity.d.ts +33 -0
  14. package/dist/backend/antigravity.js +126 -18
  15. package/dist/backend/antigravity.js.map +1 -1
  16. package/dist/backend/claude-carry.d.ts +22 -0
  17. package/dist/backend/claude-carry.js +170 -0
  18. package/dist/backend/claude-carry.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +65 -2
  20. package/dist/backend/claude-code.js +226 -33
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-metadata.d.ts +33 -0
  23. package/dist/backend/codex-metadata.js +122 -18
  24. package/dist/backend/codex-metadata.js.map +1 -1
  25. package/dist/backend/codex.d.ts +3 -0
  26. package/dist/backend/codex.js +17 -1
  27. package/dist/backend/codex.js.map +1 -1
  28. package/dist/backend/credential-profile.d.ts +40 -3
  29. package/dist/backend/credential-profile.js +113 -5
  30. package/dist/backend/credential-profile.js.map +1 -1
  31. package/dist/backend/effort-metadata.d.ts +37 -1
  32. package/dist/backend/effort-metadata.js +86 -15
  33. package/dist/backend/effort-metadata.js.map +1 -1
  34. package/dist/backend/grok.d.ts +4 -0
  35. package/dist/backend/grok.js +14 -0
  36. package/dist/backend/grok.js.map +1 -1
  37. package/dist/backend/kiro.d.ts +27 -1
  38. package/dist/backend/kiro.js +63 -10
  39. package/dist/backend/kiro.js.map +1 -1
  40. package/dist/backend/launch-effort.d.ts +61 -0
  41. package/dist/backend/launch-effort.js +170 -0
  42. package/dist/backend/launch-effort.js.map +1 -0
  43. package/dist/backend/muse.d.ts +84 -0
  44. package/dist/backend/muse.js +449 -53
  45. package/dist/backend/muse.js.map +1 -1
  46. package/dist/backend/types.d.ts +48 -2
  47. package/dist/backend/types.js +12 -1
  48. package/dist/backend/types.js.map +1 -1
  49. package/dist/backend-command.d.ts +91 -0
  50. package/dist/backend-command.js +122 -0
  51. package/dist/backend-command.js.map +1 -0
  52. package/dist/background-work-ended.d.ts +18 -0
  53. package/dist/background-work-ended.js +30 -0
  54. package/dist/background-work-ended.js.map +1 -0
  55. package/dist/cache-service.d.ts +2 -0
  56. package/dist/cache-service.js +2 -1
  57. package/dist/cache-service.js.map +1 -1
  58. package/dist/channel/adapters/discord.d.ts +26 -1
  59. package/dist/channel/adapters/discord.js +103 -3
  60. package/dist/channel/adapters/discord.js.map +1 -1
  61. package/dist/channel/adapters/telegram.d.ts +35 -3
  62. package/dist/channel/adapters/telegram.js +122 -23
  63. package/dist/channel/adapters/telegram.js.map +1 -1
  64. package/dist/channel/mcp-tools.js +5 -3
  65. package/dist/channel/mcp-tools.js.map +1 -1
  66. package/dist/channel/message-edit-errors.d.ts +5 -0
  67. package/dist/channel/message-edit-errors.js +24 -0
  68. package/dist/channel/message-edit-errors.js.map +1 -0
  69. package/dist/channel/telegram-markdown.d.ts +6 -0
  70. package/dist/channel/telegram-markdown.js +104 -0
  71. package/dist/channel/telegram-markdown.js.map +1 -0
  72. package/dist/channel/tool-router.d.ts +2 -0
  73. package/dist/channel/tool-router.js +6 -5
  74. package/dist/channel/tool-router.js.map +1 -1
  75. package/dist/channel/types.d.ts +46 -4
  76. package/dist/cli.js +184 -34
  77. package/dist/cli.js.map +1 -1
  78. package/dist/command-table.d.ts +2 -2
  79. package/dist/command-table.js +5 -2
  80. package/dist/command-table.js.map +1 -1
  81. package/dist/compact-watch.d.ts +86 -0
  82. package/dist/compact-watch.js +288 -0
  83. package/dist/compact-watch.js.map +1 -0
  84. package/dist/config-validator.js +55 -5
  85. package/dist/config-validator.js.map +1 -1
  86. package/dist/config.d.ts +1 -0
  87. package/dist/config.js +2 -1
  88. package/dist/config.js.map +1 -1
  89. package/dist/cross-instance-envelope.js +2 -0
  90. package/dist/cross-instance-envelope.js.map +1 -1
  91. package/dist/daemon.d.ts +129 -1
  92. package/dist/daemon.js +754 -94
  93. package/dist/daemon.js.map +1 -1
  94. package/dist/delivery-outbox.d.ts +64 -8
  95. package/dist/delivery-outbox.js +315 -24
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-reconciliation.d.ts +18 -1
  98. package/dist/delivery-reconciliation.js +67 -2
  99. package/dist/delivery-reconciliation.js.map +1 -1
  100. package/dist/doctor.js +7 -0
  101. package/dist/doctor.js.map +1 -1
  102. package/dist/fleet-context.d.ts +12 -1
  103. package/dist/fleet-manager.d.ts +392 -19
  104. package/dist/fleet-manager.js +2030 -275
  105. package/dist/fleet-manager.js.map +1 -1
  106. package/dist/forged-envelope-scan.d.ts +4 -3
  107. package/dist/forged-envelope-scan.js +4 -3
  108. package/dist/forged-envelope-scan.js.map +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  111. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  112. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  113. package/dist/host-memory.d.ts +10 -0
  114. package/dist/host-memory.js +33 -1
  115. package/dist/host-memory.js.map +1 -1
  116. package/dist/human-delivery-receipts.d.ts +83 -0
  117. package/dist/human-delivery-receipts.js +194 -0
  118. package/dist/human-delivery-receipts.js.map +1 -0
  119. package/dist/install-lock.d.ts +18 -1
  120. package/dist/install-lock.js +30 -3
  121. package/dist/install-lock.js.map +1 -1
  122. package/dist/instance-avatar.d.ts +5 -4
  123. package/dist/instance-avatar.js +12 -8
  124. package/dist/instance-avatar.js.map +1 -1
  125. package/dist/instance-config-impact.js +1 -0
  126. package/dist/instance-config-impact.js.map +1 -1
  127. package/dist/instance-lifecycle.d.ts +25 -6
  128. package/dist/instance-lifecycle.js +76 -27
  129. package/dist/instance-lifecycle.js.map +1 -1
  130. package/dist/instructions.d.ts +1 -1
  131. package/dist/instructions.js +3 -3
  132. package/dist/instructions.js.map +1 -1
  133. package/dist/kiro-compact-pane.d.ts +38 -0
  134. package/dist/kiro-compact-pane.js +57 -0
  135. package/dist/kiro-compact-pane.js.map +1 -0
  136. package/dist/kiro-log-gate.d.ts +42 -0
  137. package/dist/kiro-log-gate.js +39 -0
  138. package/dist/kiro-log-gate.js.map +1 -0
  139. package/dist/launchd-handoff.d.ts +43 -0
  140. package/dist/launchd-handoff.js +56 -0
  141. package/dist/launchd-handoff.js.map +1 -0
  142. package/dist/locale.js +268 -42
  143. package/dist/locale.js.map +1 -1
  144. package/dist/log-service.d.ts +99 -0
  145. package/dist/log-service.js +243 -0
  146. package/dist/log-service.js.map +1 -0
  147. package/dist/log-steps.d.ts +169 -0
  148. package/dist/log-steps.js +858 -0
  149. package/dist/log-steps.js.map +1 -0
  150. package/dist/login-controller.d.ts +20 -3
  151. package/dist/login-controller.js +71 -19
  152. package/dist/login-controller.js.map +1 -1
  153. package/dist/login-flows.d.ts +5 -2
  154. package/dist/login-flows.js +5 -4
  155. package/dist/login-flows.js.map +1 -1
  156. package/dist/login-shell.d.ts +21 -0
  157. package/dist/login-shell.js +98 -0
  158. package/dist/login-shell.js.map +1 -0
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/outbound-handlers.d.ts +100 -0
  168. package/dist/outbound-handlers.js +277 -93
  169. package/dist/outbound-handlers.js.map +1 -1
  170. package/dist/outbound-schemas.d.ts +30 -15
  171. package/dist/outbound-schemas.js +23 -5
  172. package/dist/outbound-schemas.js.map +1 -1
  173. package/dist/provider-probe.d.ts +29 -0
  174. package/dist/provider-probe.js +198 -2
  175. package/dist/provider-probe.js.map +1 -1
  176. package/dist/public-link-progress.d.ts +1 -1
  177. package/dist/public-link-progress.js +8 -2
  178. package/dist/public-link-progress.js.map +1 -1
  179. package/dist/public-preview.d.ts +101 -0
  180. package/dist/public-preview.js +236 -0
  181. package/dist/public-preview.js.map +1 -0
  182. package/dist/public-web-gateway.d.ts +2 -0
  183. package/dist/public-web-gateway.js +20 -6
  184. package/dist/public-web-gateway.js.map +1 -1
  185. package/dist/public-web-link.d.ts +24 -1
  186. package/dist/public-web-link.js +63 -9
  187. package/dist/public-web-link.js.map +1 -1
  188. package/dist/quickstart-api.d.ts +1 -1
  189. package/dist/quickstart-api.js +8 -6
  190. package/dist/quickstart-api.js.map +1 -1
  191. package/dist/quickstart.d.ts +12 -0
  192. package/dist/quickstart.js +36 -6
  193. package/dist/quickstart.js.map +1 -1
  194. package/dist/reply-buttons-controller.d.ts +89 -5
  195. package/dist/reply-buttons-controller.js +273 -8
  196. package/dist/reply-buttons-controller.js.map +1 -1
  197. package/dist/reply-buttons.d.ts +48 -3
  198. package/dist/reply-buttons.js +105 -10
  199. package/dist/reply-buttons.js.map +1 -1
  200. package/dist/reply-polls.d.ts +85 -0
  201. package/dist/reply-polls.js +154 -0
  202. package/dist/reply-polls.js.map +1 -0
  203. package/dist/restart-guard.d.ts +6 -3
  204. package/dist/restart-guard.js +6 -3
  205. package/dist/restart-guard.js.map +1 -1
  206. package/dist/restart-progress.d.ts +33 -2
  207. package/dist/restart-progress.js +30 -2
  208. package/dist/restart-progress.js.map +1 -1
  209. package/dist/sd-notify.d.ts +22 -1
  210. package/dist/sd-notify.js +63 -10
  211. package/dist/sd-notify.js.map +1 -1
  212. package/dist/service-activation.d.ts +15 -7
  213. package/dist/service-activation.js +42 -26
  214. package/dist/service-activation.js.map +1 -1
  215. package/dist/service-installer.d.ts +55 -0
  216. package/dist/service-installer.js +90 -3
  217. package/dist/service-installer.js.map +1 -1
  218. package/dist/service-plan.d.ts +11 -0
  219. package/dist/service-plan.js +23 -0
  220. package/dist/service-plan.js.map +1 -1
  221. package/dist/settings-api.js +14 -4
  222. package/dist/settings-api.js.map +1 -1
  223. package/dist/settings-change.js +4 -2
  224. package/dist/settings-change.js.map +1 -1
  225. package/dist/settings-effect.js +2 -0
  226. package/dist/settings-effect.js.map +1 -1
  227. package/dist/spawn-gate.js +5 -1
  228. package/dist/spawn-gate.js.map +1 -1
  229. package/dist/spawn-hold-reply.d.ts +12 -0
  230. package/dist/spawn-hold-reply.js +39 -0
  231. package/dist/spawn-hold-reply.js.map +1 -0
  232. package/dist/systemd-peer-bus.d.ts +95 -0
  233. package/dist/systemd-peer-bus.js +430 -0
  234. package/dist/systemd-peer-bus.js.map +1 -0
  235. package/dist/systemd-runtime.d.ts +27 -1
  236. package/dist/systemd-runtime.js +69 -7
  237. package/dist/systemd-runtime.js.map +1 -1
  238. package/dist/tips.js +1 -1
  239. package/dist/tips.js.map +1 -1
  240. package/dist/tool-permissions.js +3 -3
  241. package/dist/tool-permissions.js.map +1 -1
  242. package/dist/tool-progress.js +3 -2
  243. package/dist/tool-progress.js.map +1 -1
  244. package/dist/topic-commands.d.ts +71 -4
  245. package/dist/topic-commands.js +167 -34
  246. package/dist/topic-commands.js.map +1 -1
  247. package/dist/transcript-delta-reader.d.ts +3 -3
  248. package/dist/transcript-delta-reader.js +8 -8
  249. package/dist/transcript-delta-reader.js.map +1 -1
  250. package/dist/transcript-jsonl.d.ts +85 -3
  251. package/dist/transcript-jsonl.js +156 -13
  252. package/dist/transcript-jsonl.js.map +1 -1
  253. package/dist/transcript-monitor.d.ts +11 -0
  254. package/dist/transcript-monitor.js +61 -13
  255. package/dist/transcript-monitor.js.map +1 -1
  256. package/dist/transcript-sources.d.ts +24 -0
  257. package/dist/transcript-sources.js +211 -56
  258. package/dist/transcript-sources.js.map +1 -1
  259. package/dist/tunnel/cloudflared.d.ts +7 -15
  260. package/dist/tunnel/cloudflared.js +36 -1
  261. package/dist/tunnel/cloudflared.js.map +1 -1
  262. package/dist/tunnel/purpose-lane.d.ts +5 -1
  263. package/dist/tunnel/purpose-lane.js +14 -7
  264. package/dist/tunnel/purpose-lane.js.map +1 -1
  265. package/dist/tunnel/types.d.ts +5 -1
  266. package/dist/tunnel/types.js.map +1 -1
  267. package/dist/turn-resume.d.ts +2 -0
  268. package/dist/turn-resume.js +1 -1
  269. package/dist/turn-resume.js.map +1 -1
  270. package/dist/types.d.ts +22 -0
  271. package/dist/ui/chat-commands.js +1 -0
  272. package/dist/ui/chat-mentions.js +63 -0
  273. package/dist/ui/chat-render.js +80 -10
  274. package/dist/ui/chat-store.js +70 -6
  275. package/dist/ui/chat-strings.js +62 -32
  276. package/dist/ui/chat-thread.js +199 -24
  277. package/dist/ui/chat-time.js +80 -0
  278. package/dist/ui/chat-tour.js +17 -14
  279. package/dist/ui/panel-chat.js +45 -21
  280. package/dist/ui/panel-details.js +2 -0
  281. package/dist/ui/panel-fleet.js +213 -58
  282. package/dist/ui/panel-log.js +428 -0
  283. package/dist/ui/panel-needs.js +14 -10
  284. package/dist/ui/panel-settings.js +82 -17
  285. package/dist/ui/preview-panel.js +55 -15
  286. package/dist/ui/preview-status.js +28 -0
  287. package/dist/ui/preview.js +44 -3
  288. package/dist/ui/settings-dialogs.js +11 -6
  289. package/dist/ui/settings-model.js +3 -1
  290. package/dist/ui/settings-strings.js +32 -12
  291. package/dist/ui/settings-token.js +8 -1
  292. package/dist/ui/settings-wizard.js +5 -3
  293. package/dist/ui/shared/app-i18n.js +41 -15
  294. package/dist/ui/shared/app-prefs.js +250 -0
  295. package/dist/ui/shared/app-route.js +6 -1
  296. package/dist/ui/shared/app-shell.js +165 -8
  297. package/dist/ui/shared/app-store.js +5 -1
  298. package/dist/ui/shared/app-stream.js +26 -5
  299. package/dist/ui/shared/app.css +170 -30
  300. package/dist/ui/shared/app.js +11 -1
  301. package/dist/ui/shared/fonts/README.md +1 -1
  302. package/dist/ui/shared/header-tools.js +83 -13
  303. package/dist/ui/shared/instance-nav.js +178 -51
  304. package/dist/ui/shared/instance-switch.js +8 -5
  305. package/dist/ui/shared/panel-view.js +21 -4
  306. package/dist/ui/shared/signin.js +15 -9
  307. package/dist/ui/shared/theme.js +39 -7
  308. package/dist/ui/shared/tokens.css +1 -1
  309. package/dist/ui/shared/ui-icons.js +2 -0
  310. package/dist/ui/shared/usage-dialog.js +23 -5
  311. package/dist/ui/shared/view-strings.js +40 -4
  312. package/dist/ui/signin.html +8 -3
  313. package/dist/unrecognised-prompt.d.ts +23 -0
  314. package/dist/unrecognised-prompt.js +74 -0
  315. package/dist/unrecognised-prompt.js.map +1 -0
  316. package/dist/update-check.d.ts +31 -0
  317. package/dist/update-check.js +47 -0
  318. package/dist/update-check.js.map +1 -1
  319. package/dist/update-install.d.ts +13 -1
  320. package/dist/update-install.js +16 -4
  321. package/dist/update-install.js.map +1 -1
  322. package/dist/update-launch.d.ts +48 -1
  323. package/dist/update-launch.js +159 -5
  324. package/dist/update-launch.js.map +1 -1
  325. package/dist/update-marker.d.ts +51 -0
  326. package/dist/update-marker.js +61 -0
  327. package/dist/update-marker.js.map +1 -1
  328. package/dist/usage/i18n-keys.d.ts +1 -1
  329. package/dist/usage/i18n-keys.js +1 -1
  330. package/dist/usage/i18n-keys.js.map +1 -1
  331. package/dist/usage/providers.d.ts +62 -13
  332. package/dist/usage/providers.js +256 -68
  333. package/dist/usage/providers.js.map +1 -1
  334. package/dist/usage/statusline-usage.d.ts +6 -2
  335. package/dist/usage/statusline-usage.js +10 -4
  336. package/dist/usage/statusline-usage.js.map +1 -1
  337. package/dist/view-api.js +31 -9
  338. package/dist/view-api.js.map +1 -1
  339. package/dist/web-api.d.ts +82 -4
  340. package/dist/web-api.js +632 -88
  341. package/dist/web-api.js.map +1 -1
  342. package/dist/web-auth.js +5 -2
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-chat-history.d.ts +26 -1
  345. package/dist/web-chat-history.js +47 -8
  346. package/dist/web-chat-history.js.map +1 -1
  347. package/dist/web-commands.d.ts +41 -5
  348. package/dist/web-commands.js +25 -7
  349. package/dist/web-commands.js.map +1 -1
  350. package/dist/web-login.d.ts +14 -0
  351. package/dist/web-login.js +4 -2
  352. package/dist/web-login.js.map +1 -1
  353. package/dist/web-mentions.d.ts +44 -0
  354. package/dist/web-mentions.js +75 -0
  355. package/dist/web-mentions.js.map +1 -0
  356. package/dist/web-prefs.d.ts +81 -0
  357. package/dist/web-prefs.js +297 -0
  358. package/dist/web-prefs.js.map +1 -0
  359. package/dist/web-preview.d.ts +14 -1
  360. package/dist/web-preview.js +35 -13
  361. package/dist/web-preview.js.map +1 -1
  362. package/dist/web-profile.d.ts +32 -0
  363. package/dist/web-profile.js +132 -0
  364. package/dist/web-profile.js.map +1 -0
  365. package/dist/web-reply-files.d.ts +45 -0
  366. package/dist/web-reply-files.js +223 -0
  367. package/dist/web-reply-files.js.map +1 -0
  368. package/dist/web-schedules.d.ts +12 -0
  369. package/dist/web-schedules.js +34 -0
  370. package/dist/web-schedules.js.map +1 -0
  371. package/dist/web-server-drain.d.ts +10 -0
  372. package/dist/web-server-drain.js +50 -0
  373. package/dist/web-server-drain.js.map +1 -0
  374. package/dist/web-session.d.ts +14 -0
  375. package/dist/web-session.js +11 -1
  376. package/dist/web-session.js.map +1 -1
  377. package/dist/web-settings-patch.d.ts +3 -1
  378. package/dist/web-settings-patch.js +4 -1
  379. package/dist/web-settings-patch.js.map +1 -1
  380. package/dist/web-shell-routes.d.ts +4 -0
  381. package/dist/web-shell-routes.js +9 -0
  382. package/dist/web-shell-routes.js.map +1 -1
  383. package/dist/web-upload.d.ts +135 -7
  384. package/dist/web-upload.js +315 -51
  385. package/dist/web-upload.js.map +1 -1
  386. package/launcher/install-admission.cjs +61 -6
  387. package/package.json +3 -3
  388. package/templates/launchd.plist.ejs +3 -1
  389. package/templates/systemd.service.ejs +2 -1
@@ -68,10 +68,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
68
68
 
69
69
  .sidebar {
70
70
  width: var(--sidebar-w); flex-shrink: 0; display: flex; flex-direction: column; background: var(--bg-sidebar);
71
- padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) env(safe-area-inset-left);
71
+ position: relative; /* #1722: the resize handle sits on its right edge (the width above: tokens.css, or this device's) */
72
+ padding: 0 0 env(safe-area-inset-bottom) env(safe-area-inset-left); /* the top inset is the head's, as the page header's is */
72
73
  }
73
74
  .sb-collapsed .sidebar { display: none; }
74
- .side-head { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-3) var(--s-2) var(--s-2) var(--s-4); }
75
+ /* The brand row is the page header's row: the same height, padding and centre line (--header-h; .panel-head, .panel-title). */
76
+ .side-head { display: flex; align-items: center; gap: var(--s-1); box-sizing: content-box; min-height: calc(var(--header-h) - 2 * var(--s-1));
77
+ padding: max(var(--s-1), env(safe-area-inset-top)) var(--s-2) var(--s-1) var(--s-4); }
75
78
  .brand { flex: 1; font-size: var(--fs-lg); font-weight: var(--fw-semibold); letter-spacing: -0.01em; }
76
79
  .side-nav { display: flex; flex-direction: column; padding: var(--s-1) var(--s-2); gap: 1px; }
77
80
  .side-row {
@@ -112,18 +115,23 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
112
115
  .av.ring { box-shadow: 0 0 0 2px var(--ring, transparent); }
113
116
  .av-text { overflow: hidden; }
114
117
  .av .av-dot { position: absolute; right: -2px; bottom: -2px; margin: 0; box-shadow: 0 0 0 2px var(--bg-sidebar); }
118
+ /* #1722: the CLI as a letter on the avatar's top-left corner — monochrome (the #1630 ring keeps the colour), cut out like the dot */
119
+ .av-cli { position: absolute; left: -4px; top: -4px; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
120
+ background: var(--surface); border: 1px solid var(--border-strong); box-shadow: 0 0 0 1.5px var(--bg-sidebar);
121
+ color: var(--text-2); font-size: 9px; line-height: 12px; font-weight: var(--fw-semibold); text-align: center; pointer-events: auto; }
122
+ .inst.active .av .av-cli, .inst:hover .av .av-cli { box-shadow: 0 0 0 1.5px var(--surface-2); }
115
123
  .inst.active .av .av-dot, .inst:hover .av .av-dot { box-shadow: 0 0 0 2px var(--surface-2); }
116
124
  .dot.hollow { background: transparent; border: 1.5px solid var(--text-3); }
117
125
  /* On a grey avatar a grey dot needs more contrast: stopped is a stronger grey, paused a clear ring around the sidebar colour. */
118
126
  .av .av-dot.off { background: var(--text-2); }
119
127
  .av .av-dot.hollow { box-sizing: border-box; width: 10px; height: 10px; background: var(--bg-sidebar); border: 2.5px solid var(--text-2); }
120
128
  .inst .av { margin-top: 1px; }
121
- .inst-id { font-family: var(--font-mono); }
122
129
  .inst-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
123
130
  .inst-name { font-size: var(--fs-sm); line-height: var(--lh-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
131
+ /* #1722: the other name, under the one that leads — smaller, muted, truncated on its own */
132
+ .inst-alt { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
124
133
  .inst.active .inst-name { font-weight: var(--fw-semibold); }
125
134
  .inst-sub { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); }
126
- .inst-alias { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; }
127
135
  .badge-await {
128
136
  flex-shrink: 0; font-size: 11px; line-height: 16px; font-weight: var(--fw-semibold); padding: 0 var(--s-2); border-radius: var(--r-full);
129
137
  background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-line); white-space: nowrap;
@@ -144,6 +152,10 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
144
152
  .s-device { display: flex; flex-direction: column; gap: var(--s-3); }
145
153
  .s-device .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
146
154
  .s-device-row { display: flex; gap: var(--s-2); }
155
+ /* #1827: the person's web name — its field takes the card's width; a refusal reads in the danger colour. */
156
+ .s-profile .field { display: flex; flex-direction: column; gap: var(--s-1); }
157
+ .s-profile .field input { max-width: 24rem; }
158
+ .s-profile .err { color: var(--danger); }
147
159
  /* #1620: Settings → Web — the lead, and the read-only list of what is set in fleet.yaml. */
148
160
  .s-web .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
149
161
  .s-web-fields:disabled select { opacity: 0.6; } /* over the public link: dimmed like the inputs (.field input:disabled) */
@@ -189,20 +201,23 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
189
201
  }
190
202
  .panel-head .sb-open { display: none; }
191
203
  .sb-collapsed .panel-head .sb-open { display: inline-flex; }
192
- .panel-title { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--s-3); }
204
+ /* One row of --header-h on every page (a phone's instance tabs wrap below it): the title, its status and badges on one centre line. */
205
+ .panel-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s-3); min-height: calc(var(--header-h) - 2 * var(--s-1)); }
193
206
  .panel-title h1 { font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; outline: none; }
194
207
  .panel-sub { color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
195
208
  .status { display: inline-flex; align-items: center; gap: 6px; }
196
209
  .panel-actions { display: flex; align-items: center; gap: var(--s-1); }
197
210
  /* #1523 N2: the instance view switch — beside the title on a wide screen; on a phone, a row of tabs under the header. */
198
211
  .inst-switch.seg { padding: 0; margin: 0; width: auto; max-width: none; flex-shrink: 0; }
199
- .inst-switch .seg-item { min-height: 30px; }
212
+ .inst-switch .seg-item { min-height: 30px; flex-direction: column; justify-content: center; gap: 0; }
213
+ /* The label again in the current tab's weight, taking width but no height and never seen: the bar's size does not change with the tab. */
214
+ .inst-switch .seg-item::after { content: attr(data-label); font-weight: var(--fw-semibold); height: 0; overflow: hidden; visibility: hidden; pointer-events: none; user-select: none; }
200
215
  /* #1523 N3 (Q2 = A): the text size, one custom property from the root's data-text-size (header-tools.js). The chat's
201
216
  messages, Details and the Fleet tabs scale the type tokens by it; View's terminal scales itself (VIEW_SCALE). */
202
217
  :root { --text-scale: 1; }
203
218
  :root[data-text-size="s"] { --text-scale: 0.9; }
204
219
  :root[data-text-size="l"] { --text-scale: 1.15; }
205
- .thread, .p-details .panel-body, .p-fleet .panel-body {
220
+ .thread, .p-details .panel-body, .p-fleet .panel-body, .p-log .panel-body {
206
221
  --fs-xs: calc(12px * var(--text-scale)); --lh-xs: calc(16px * var(--text-scale));
207
222
  --fs-sm: calc(13px * var(--text-scale)); --lh-sm: calc(18px * var(--text-scale));
208
223
  --fs-md: calc(15px * var(--text-scale)); --lh-md: calc(24px * var(--text-scale));
@@ -211,10 +226,44 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
211
226
  }
212
227
  /* #1523 N2: Details. */
213
228
  .p-details .d-tags { display: inline-flex; flex-wrap: wrap; gap: var(--s-1); }
229
+ /* #1218: an instance's Log — one column of steps, newest at the bottom; the filter bar above it. Classes only (#1300). */
230
+ .p-log .log-bar { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2) var(--s-4); border-bottom: 1px solid var(--border); flex: none; }
231
+ .p-log .log-bar .chip-btn.on { color: var(--text); background: var(--surface-2); font-weight: var(--fw-semibold); }
232
+ .p-log .log-live { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-3); }
233
+ .p-log .log-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-1); }
234
+ .p-log .log-item { padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface); min-width: 0; overflow-wrap: anywhere; }
235
+ .p-log .log-item.turn-start { margin-top: var(--s-4); }
236
+ .p-log .log-item.k-user { background: var(--user-bubble); }
237
+ .p-log .log-item.k-turn_end, .p-log .log-item.k-state, .p-log .log-item.k-event { background: transparent; color: var(--text-3); padding-block: 2px; font-size: var(--fs-sm); }
238
+ .p-log .log-item.k-error, .p-log .log-item.s-failed { border-left: 3px solid var(--danger); }
239
+ .p-log .log-head { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
240
+ .p-log .log-icon { flex: none; }
241
+ .p-log .log-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
242
+ .p-log .log-cat { font-weight: var(--fw-semibold); }
243
+ .p-log .log-subject { font-size: var(--fs-sm); }
244
+ .p-log .log-time { flex: none; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
245
+ .p-log .log-status { flex: none; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
246
+ .p-log .log-status.done { color: var(--ok); }
247
+ .p-log .log-status.failed { color: var(--danger); }
248
+ .p-log .log-status.interrupted { color: var(--warn, var(--text-3)); }
249
+ .p-log .log-status .spinner { display: inline-block; width: 8px; height: 8px; margin-right: 4px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }
250
+ .p-log .log-repeat { flex: none; font-size: var(--fs-xs); padding: 0 6px; border-radius: 8px; background: var(--surface-2); color: var(--text-2); }
251
+ .p-log .log-text { white-space: pre-wrap; margin-top: var(--s-1); font-size: var(--fs-sm); }
252
+ .p-log .log-text.mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
253
+ .p-log .log-text.folded { max-height: 10.5em; overflow: hidden; mask-image: linear-gradient(to bottom, #000 70%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent); }
254
+ .p-log .link-btn { border: 0; background: none; padding: 2px 0; color: var(--accent); cursor: pointer; font-size: var(--fs-xs); }
255
+ .p-log .log-detail { margin-top: var(--s-1); display: flex; flex-direction: column; gap: var(--s-2); }
256
+ .p-log .log-sec-h { font-size: var(--fs-xs); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
257
+ .p-log .log-older, .p-log .log-start { text-align: center; margin: var(--s-2) 0; }
258
+ .p-log .log-off { padding: var(--s-6) var(--s-4); }
214
259
  .p-details .d-edit { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-2); }
260
+ /* The header's controls are one height, the icon buttons' (36 px): its small buttons, and the instance tabs when they share
261
+ its row (a phone puts them on a row of their own, under it). */
262
+ .panel-head .btn-sm { min-height: 36px; }
263
+ @media (min-width: 900px) { .panel-head .inst-switch .seg-item { min-height: 36px; } }
215
264
  @media (max-width: 899px) {
216
265
  .panel-head:has(.inst-switch) { flex-wrap: wrap; }
217
- .inst-switch.seg { order: 3; flex-basis: 100%; display: grid; grid-template-columns: repeat(3, 1fr); padding: 0 0 var(--s-1); }
266
+ .inst-switch.seg { order: 3; flex-basis: 100%; display: grid; grid-template-columns: repeat(4, 1fr); padding: 0 0 var(--s-1); }
218
267
  .inst-switch .seg-item { justify-content: center; border-radius: var(--r-sm); }
219
268
  }
220
269
  .panel-body { flex: 1; min-height: 0; overflow-y: auto; }
@@ -228,8 +277,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
228
277
  .empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
229
278
  .empty-hint { font-size: var(--fs-sm); }
230
279
  .empty .btn { margin-top: var(--s-3); }
231
- /* #1519 P7: the first-run card (a fleet with no chat connection), above an empty chat or a Settings list. */
232
- .first-run-stack { width: 100%; max-width: var(--col); }
280
+ /* #1519 P7: the first-run card (a fleet with no chat connection), above a Settings list (#1854: no longer on Chat). */
233
281
  .first-run h3 { display: flex; align-items: center; gap: var(--s-2); }
234
282
  .first-run p { color: var(--text-2); font-size: var(--fs-sm); }
235
283
  .error-state { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding: var(--s-8) var(--s-4); color: var(--danger); text-align: center; }
@@ -307,6 +355,16 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
307
355
  .pill.open { background: var(--accent-dim); color: var(--accent); }
308
356
  .pill.claimed { background: var(--warn-bg); color: var(--warn); }
309
357
  .pill.done { color: var(--ok); }
358
+ /* #1724: a disabled schedule — its own look (not the green of "done"): a dashed, muted badge and a dimmed row */
359
+ .pill.off { background: transparent; color: var(--text-3); border: 1px dashed var(--border-strong); }
360
+ .row-item.is-off { opacity: 0.62; }
361
+ .row-item.sched { flex-wrap: nowrap; }
362
+ .row-item.sched .sched-text { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 2px; }
363
+ .sched-actions { display: inline-flex; flex: none; gap: var(--s-1); }
364
+ .sched-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
365
+ .sched-sub { font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
366
+ @media (max-width: 899px) { .sched-sub { white-space: normal; } } /* a phone: the run time wraps rather than being cut */
367
+ .sched-mode { margin-bottom: var(--s-2); }
310
368
  .save-row { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
311
369
 
312
370
  /* ── Chat ── */
@@ -315,11 +373,31 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
315
373
  .msg { font-size: var(--fs-md); line-height: var(--lh-md); overflow-wrap: anywhere; min-width: 0; }
316
374
  .msg .meta { display: flex; align-items: baseline; gap: var(--s-2); font-size: var(--fs-xs); color: var(--text-3); margin-bottom: var(--s-1); }
317
375
  .msg .sender { font-weight: var(--fw-semibold); color: var(--text-2); }
318
- .msg .time { font-family: var(--font-mono); }
376
+ /* The time never wraps or gives up its width to a long neighbour: a flex child shrinks to its min-content, and with
377
+ the message's overflow-wrap: anywhere that is one character wide (the #1672 status line's time, one glyph per line). */
378
+ .msg .time { font-family: var(--font-mono); white-space: nowrap; flex: none; }
319
379
  .msg.user { align-self: flex-end; max-width: min(80%, 620px); }
320
380
  .msg.user .meta { justify-content: flex-end; }
321
- .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); }
381
+ /* #1773: the bubble hugs its text. The message is as wide as its widest child — usually the meta row (sender, a time
382
+ such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
383
+ the right under its meta, and a long one still wraps at the message's max-width. */
384
+ .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
385
+ /* A person's click on a reply button: the bubble shows 🔘 and the button's label (never the value the agent was given);
386
+ live, the label is a button that shows the reply its buttons were on. */
387
+ .click-chip { display: flex; }
388
+ .click-chip .click-link, .click-chip .click-text { display: inline-flex; align-items: baseline; gap: var(--s-1); padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; }
389
+ .click-chip .click-link { cursor: pointer; }
390
+ .click-chip .click-link:hover, .click-chip .click-link:focus-visible { text-decoration: underline; }
391
+ .click-mark { flex: none; }
322
392
  .msg.agent { align-self: stretch; }
393
+ /* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
394
+ Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
395
+ the meta, body and tools into one flex row. */
396
+ .msg.status-line { align-self: stretch; font-size: var(--fs-sm); color: var(--text-3); }
397
+ .msg.status-line .st-text { white-space: pre-wrap; border-left: 2px solid var(--border); padding-left: var(--s-3); }
398
+ .msg.status-line.folded .st-text { max-height: 9em; overflow: hidden; mask-image: linear-gradient(to bottom, #000 60%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); }
399
+ .msg.status-line .st-more { border: 0; background: none; color: var(--text-2); cursor: pointer; font-size: var(--fs-xs); padding: var(--s-1) 0 0 var(--s-3); }
400
+ .msg.status-line .st-more:hover { color: var(--text); }
323
401
  .msg-tools { display: flex; gap: var(--s-1); margin-top: var(--s-1); opacity: 0; transition: opacity var(--t-fast); }
324
402
  .msg:hover .msg-tools, .msg:focus-within .msg-tools { opacity: 1; }
325
403
  @media (hover: none) { .msg-tools { opacity: 1; } }
@@ -341,10 +419,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
341
419
  .text-card { margin-top: var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--s-2) var(--s-3); }
342
420
  .text-card .tc-head { display: flex; align-items: center; gap: var(--s-2); }
343
421
  .text-card .tc-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-xs); font-family: var(--font-mono); color: var(--text-2); }
344
- .text-card .tc-body { margin-top: var(--s-2); max-height: 480px; overflow: auto; }
422
+ /* #1812: the scroller spans the card's padding box and insets its own content — 12px on both sides, from the border on
423
+ the left and from the scrollbar (or the border, with overlay scrollbars) on the right. A stable gutter keeps a
424
+ classic scrollbar off the content, and the content from shifting when the scrollbar comes and goes. */
425
+ .text-card .tc-body { margin: var(--s-2) calc(-1 * var(--s-3)) 0; padding: 0 var(--s-3); max-height: 480px; overflow: auto; scrollbar-gutter: stable; }
345
426
  .text-card .tc-body[hidden] { display: none; }
346
427
  .text-card .tc-pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: var(--fs-xs); }
347
- .text-card .tc-table-wrap { overflow-x: auto; }
428
+ .text-card .tc-table-wrap { overflow-x: auto; padding-bottom: var(--s-3); }
348
429
  .text-card .tc-table { border-collapse: collapse; font-size: var(--fs-xs); }
349
430
  .text-card .tc-table th, .text-card .tc-table td { border: 1px solid var(--border); padding: 2px 6px; text-align: left; vertical-align: top; }
350
431
  .text-card .tc-more { margin-top: var(--s-2); }
@@ -377,10 +458,17 @@ html.lb-open .scroller { overflow: hidden; }
377
458
  .msg .md ul { list-style: disc; } .msg .md ol { list-style: decimal; }
378
459
  .msg .md li > ul, .msg .md li > ol { margin: 2px 0; }
379
460
  .msg .md hr { border: 0; border-top: 1px solid var(--border); margin: var(--s-3) 0; }
380
- .msg .md .tbl { overflow-x: auto; margin: var(--s-2) 0; }
461
+ /* #1812: a table too wide for its message scrolls in this wrapper (see the cell rule below); the bottom padding keeps an
462
+ overlay scrollbar off the last row. */
463
+ .msg .md .tbl { overflow-x: auto; margin: var(--s-2) 0 0; padding-bottom: var(--s-3); }
381
464
  .msg .md table { border-collapse: collapse; font-size: var(--fs-sm); }
382
465
  .msg .md th, .msg .md td { border: 1px solid var(--border); padding: 5px 9px; text-align: left; vertical-align: top; }
383
466
  .msg .md th { background: var(--surface); font-weight: var(--fw-semibold); }
467
+ /* #1812: table cells keep their words whole. The message's `overflow-wrap: anywhere` also shrinks a cell's minimum
468
+ width, so auto table layout squeezed the columns and split words ("directo|r"); `break-word` breaks a word only when
469
+ it alone is wider than its line and leaves the minimum at whole words, so a wide table overflows into its scrolling
470
+ wrapper instead. A column keeps at least 5em: CJK text can break between any two characters. */
471
+ .msg .md th, .msg .md td, .text-card .tc-table th, .text-card .tc-table td { overflow-wrap: break-word; word-break: normal; min-width: 5em; }
384
472
  .msg .md .al-c { text-align: center; } .msg .md .al-r { text-align: right; }
385
473
  .msg .md h3, .msg .md h4, .msg .md h5, .msg .md h6 { margin: var(--s-3) 0 var(--s-2); font-size: var(--fs-md); font-weight: var(--fw-semibold); }
386
474
  .msg .md .tk-k { color: var(--tk-k); } .msg .md .tk-s { color: var(--tk-s); } .msg .md .tk-n { color: var(--tk-n); } .msg .md .tk-c { color: var(--tk-c); font-style: italic; }
@@ -457,6 +545,10 @@ html.lb-open .scroller { overflow: hidden; }
457
545
  .rb-btn { max-width: 100%; white-space: normal; text-align: left; }
458
546
  .rb-btn.chosen, .rb-btn.chosen:disabled { border-color: var(--ok); color: var(--text); background: color-mix(in srgb, var(--ok) 14%, transparent); font-weight: var(--fw-semibold); opacity: 1; }
459
547
  .rb-btn:disabled:not(.chosen) { opacity: 0.55; }
548
+ /* #1823: a poll — its closed buttons show the tally, so they stay readable; the time left keeps its width. */
549
+ .rb-row.poll .rb-btn:disabled { opacity: 0.85; }
550
+ .rb-row.poll .poll-left { font-variant-numeric: tabular-nums; }
551
+ .poll-voters { margin: var(--s-1) 0 0; padding-left: var(--s-4); color: var(--text-2); font-size: var(--fs-sm); }
460
552
  .rb-note { font-size: var(--fs-xs); color: var(--text-3); }
461
553
  .pv-menu { position: relative; }
462
554
  .pv-menu > summary { list-style: none; cursor: pointer; padding: 2px var(--s-2); border-radius: var(--r-sm); color: var(--text-3); }
@@ -589,11 +681,32 @@ html.lb-open .scroller { overflow: hidden; }
589
681
  .caret.folded { transform: rotate(-90deg); }
590
682
  .caret.flipped { transform: rotate(180deg); } /* the card's toggle: up when closed (it opens upwards) */
591
683
  .v-inst { align-items: center; }
684
+ /* #1664: favorites — the star after a row (on hover or focus on a desktop, always when starred; on a phone in Edit) */
685
+ .inst-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
686
+ .inst-head .side-label { flex: 1; min-width: 0; }
687
+ .fav-edit { margin-right: var(--s-2); display: inline-flex; align-items: center; gap: var(--s-1); }
688
+ .v-li { display: flex; align-items: center; gap: 2px; }
689
+ .v-li > .inst { flex: 1; min-width: 0; }
690
+ .inst-star, .inst-move { flex: none; width: 28px; height: 28px; padding: 0; color: var(--text-3); }
691
+ .inst-star.on { color: var(--warn); }
692
+ .inst-star.on svg, .fav-mark { fill: currentColor; }
693
+ .fav-head { cursor: default; }
694
+ .fav-mark { color: var(--warn); }
695
+ .inst-nav:not(.fav-editing) .inst-star:not(.on) { visibility: hidden; }
696
+ /* As specific as the rule that hides it (and later), so hover and focus win. */
697
+ @media (hover: hover) { .inst-nav:not(.fav-editing) .v-li:hover .inst-star, .inst-nav:not(.fav-editing) .v-li:focus-within .inst-star { visibility: visible; } }
698
+ @media (hover: none) { .inst-nav:not(.fav-editing) .inst-star { display: none; } }
699
+ /* Edit: room for ↑ ↓ ☆ — the context and CLI marks step aside so the name keeps its width. */
700
+ .fav-editing .v-li .v-meta { display: none; }
701
+ @media (max-width: 899px) { .fav-editing .inst-star, .fav-editing .inst-move { width: 36px; height: 36px; } }
592
702
  .v-inst .dot { margin-top: 0; }
593
703
  .v-meta { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
594
- .cli-icon { width: 10px; height: 10px; flex: 0 0 10px; border-radius: 50%; border: 1px solid var(--border-strong); }
595
- .cli-claude-code { background: #c678dd; } .cli-kiro-cli { background: #58a6ff; } .cli-codex { background: #3fb950; }
596
- .cli-grok { background: #0d1117; border-color: #f0f6fc; } .cli-antigravity { background: #f85149; } .cli-opencode { background: #f0f6fc; } .cli-other { background: #6e7681; }
704
+ /* #1722: the sidebar's width — a separator on its right edge (drag, arrows, Home/End, double-click resets); not on a phone */
705
+ .side-resize { position: absolute; top: 0; right: -3px; width: 6px; height: 100%; z-index: 5; cursor: col-resize; touch-action: none; }
706
+ .side-resize:hover, .side-resize:focus-visible, .side-resize.dragging { background: var(--accent-dim, var(--border-strong)); }
707
+ .side-resize:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
708
+ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-select: none; }
709
+ @media (max-width: 899px) { .side-resize { display: none; } }
597
710
  .dragging { opacity: 0.4; }
598
711
  .drop-before { box-shadow: inset 0 2px 0 var(--accent); }
599
712
  .drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
@@ -604,21 +717,35 @@ html.lb-open .scroller { overflow: hidden; }
604
717
  .v-filter-box input::placeholder { color: var(--text-3); }
605
718
  .v-filter-clear { width: 28px; height: 28px; }
606
719
  .v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
607
- /* alpha.2 (N1): status and CLI chips under the filter box — each a <details> with checkboxes. */
608
- .nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
609
- .nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
610
- .nav-facet > summary { cursor: pointer; list-style: none; padding: 2px var(--s-2); border-radius: var(--r-sm); }
611
- .nav-facet > summary::-webkit-details-marker { display: none; }
612
- .nav-facet > summary::after { content: " ▾"; color: var(--text-3); }
613
- .nav-facet[open] > summary::after { content: " ▴"; }
614
- .nav-facet > summary:hover { background: var(--hover); }
615
- .nav-facet > summary:focus-visible { outline: 2px solid var(--accent); }
720
+ /* alpha.2 (N1): status and CLI chips under the filter box — each a group of checkboxes under its header (#1700). */
721
+ /* #1776: the "N / M" count is on the headers' row, at its right end — not a row of its own. The headers (and an open
722
+ group's list, the reset) wrap in their own box beside it, so a filter's summary or a narrow sidebar moves a header
723
+ down, never the count; the count is as tall as a header, so the row does not grow. */
724
+ .nav-facets { display: flex; align-items: flex-start; gap: var(--s-2); padding: var(--s-2) var(--s-1) 0; }
725
+ .nav-facet-groups { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); flex: 1 1 auto; min-width: 0; }
726
+ .nav-facets .nav-shown { flex: none; margin: 0 0 0 auto; padding: 2px 0; white-space: nowrap; }
727
+ .nav-facet { font-size: var(--fs-xs); color: var(--text-2); min-width: 0; max-width: 100%; }
728
+ /* #1700: a facet group's header — the whole row toggles its list; folded with a filter set, a short summary. */
729
+ .nav-facet-head { display: inline-flex; align-items: center; gap: var(--s-1); max-width: 100%; padding: 2px var(--s-2); border: 0; border-radius: var(--r-sm);
730
+ background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; white-space: nowrap; }
731
+ .nav-facet-head:hover { background: var(--hover); }
732
+ .nav-facet-head:focus-visible { outline: 2px solid var(--accent); }
733
+ .nav-facet-head .caret { color: var(--text-3); flex: none; }
734
+ .nav-facet-sum { min-width: 0; max-width: 12em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--accent); }
616
735
  .nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
617
736
  .nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
618
737
  .nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
619
738
  .nav-facet-reset { font-size: var(--fs-xs); }
620
739
 
621
- .p-view .panel-actions .btn { gap: var(--s-1); }
740
+ /* One gap on every page (View's, #1448): the header's controls are the same size on every tab. */
741
+ .panel-actions .btn { gap: var(--s-1); }
742
+ /* Aa's names share one cell, only the current one shown: the button is as wide as its widest name on every page. */
743
+ .hd-text-names { display: inline-grid; }
744
+ .hd-text-names > span { grid-area: 1 / 1; visibility: hidden; }
745
+ .hd-text-names > .on { visibility: visible; }
746
+ .hd-slot { visibility: hidden; pointer-events: none; }
747
+ /* #1844 Fleet overview → Docs: the arrow says "opens a new tab". */
748
+ .hd-docs .hd-ext { color: var(--text-3); }
622
749
  .v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
623
750
  .v-pre { margin: 0; min-width: 100%; min-height: 100%; font-family: var(--font-mono); line-height: 1.2; white-space: pre; color: #d0d0d0; }
624
751
  .v-pre .ansi[data-b] { font-weight: var(--fw-bold); }
@@ -642,8 +769,11 @@ html.lb-open .scroller { overflow: hidden; }
642
769
  .help-list { list-style: disc; padding-left: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); font-size: var(--fs-sm); }
643
770
  .err { color: var(--danger); font-size: var(--fs-sm); }
644
771
  .u-provider { padding: var(--s-3) var(--s-4); border-radius: var(--r-md); background: var(--surface); display: flex; flex-direction: column; gap: var(--s-2); }
645
- .u-head { display: flex; align-items: center; gap: var(--s-2); }
646
- .u-order { margin-left: auto; display: flex; gap: 2px; }
772
+ .u-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
773
+ /* #1749: one line beside ↑↓ — the name gives way first, then the plan badge (each with an ellipsis); ↑↓ never wrap. */
774
+ .u-head strong { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
775
+ .u-plan { min-width: 0; flex: none; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
776
+ .u-order { margin-left: auto; display: flex; gap: 2px; flex: none; }
647
777
  .u-order .icon-btn { width: 28px; height: 28px; }
648
778
  .u-order .icon-btn:disabled { opacity: 0.3; }
649
779
  .u-metric { display: flex; flex-direction: column; gap: var(--s-1); }
@@ -660,6 +790,12 @@ html.lb-open .scroller { overflow: hidden; }
660
790
  .u-sub { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
661
791
  .u-err { color: var(--warn); font-size: var(--fs-sm); }
662
792
  .u-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
793
+ /* #1673: the usage rail beside the page on a wide screen (header-tools.js UsageRail; shown only at min-width 1200px). */
794
+ .u-rail { width: 320px; flex-shrink: 0; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--border); background: var(--bg); }
795
+ .u-rail-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); padding: var(--s-3) var(--s-3) var(--s-2) var(--s-4); }
796
+ .u-rail-head h2 { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); }
797
+ .u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
798
+ @media (max-width: 1199px) { .u-rail { display: none; } }
663
799
  @media (max-width: 899px) {
664
800
  .hide-narrow { display: none; }
665
801
  .v-card { padding-left: var(--s-3); padding-right: var(--s-3); }
@@ -946,6 +1082,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
946
1082
  .cmd-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
947
1083
  .cmd-name { font-weight: var(--fw-semibold); }
948
1084
  .cmd-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-ui); color: var(--text-2); }
1085
+ .cmd-phase { color: var(--text); font-weight: var(--fw-semibold); }
949
1086
  .cmd-choices { display: flex; flex-wrap: wrap; gap: var(--s-1); max-height: 180px; overflow-y: auto; }
950
1087
  .cmd-choices .btn.current { border-color: var(--accent); color: var(--accent); }
951
1088
  .quick-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
@@ -959,3 +1096,6 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
959
1096
  }
960
1097
  /* #1630: a Discord server emoji in a chat message, the size of the text around it. */
961
1098
  .md img.emoji { display: inline-block; width: 1.375em; height: 1.375em; object-fit: contain; vertical-align: -0.3em; margin: 0 0.05em; }
1099
+ /* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
1100
+ .md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
1101
+ font-weight: var(--fw-medium); white-space: nowrap; }
@@ -5,6 +5,8 @@
5
5
  // session, and so is View (panel-view.js, loaded on demand). The panels that need a session live under /ui/js/ and are
6
6
  // reached only by the dynamic imports below, in "full" mode. A test walks the static imports over real HTTP and asserts
7
7
  // none of them names /ui/.
8
+ // #1665 first: the host's preferences go into this browser's cache before any module below reads its key.
9
+ import { bootPrefs } from "./app-prefs.js";
8
10
  import { html, render } from "./app-html.js";
9
11
  import { Shell, handleKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener } from "./app-shell.js";
10
12
  import { installSwipe } from "./app-swipe.js";
@@ -15,11 +17,12 @@ import { chatPath } from "./app-route.js";
15
17
  import { startNeedsNotifier } from "./app-needs.js";
16
18
  import { toast } from "./ui-toast.js";
17
19
  import { t } from "./app-i18n.js";
18
- import { initTextSize } from "./header-tools.js";
20
+ import { initTextSize, initUsageRail } from "./header-tools.js";
19
21
 
20
22
  const boot = document.body.dataset;
21
23
  // #1523 N3: this device's text size, on the root before the first paint of any panel.
22
24
  initTextSize();
25
+ initUsageRail();
23
26
  // "full": a signed-in page. "view-only": an anonymous reader of /view under web.view_access: open — no stream, no
24
27
  // session-only panel, nothing under /ui/ is ever imported or read (#1408 §3).
25
28
  const mode = boot.mode === "view-only" ? "view-only" : "full";
@@ -48,6 +51,8 @@ if (mode === "full") {
48
51
  stream.on("hydration", (hydration) => appStore.set({ hydration }));
49
52
  appStore.set({ retryHydration: () => stream.catchUp() });
50
53
  stream.on("needs", (d) => appStore.set({ needs: Array.isArray(d && d.items) ? d.items : [] }));
54
+ // #1218: a panel's way to hear one stream event (the Log's steps_tick), unsubscribed by its lease.
55
+ appStore.set({ streamOn: (name, fn) => stream.on(name, fn) });
51
56
  appStore.set({ connection: stream.connection() });
52
57
 
53
58
  // The chat's store must hear every message from the first frame, whatever panel is open: its module boots once,
@@ -64,6 +69,7 @@ if (mode === "full") {
64
69
  const loadSettings = retryable((a) => import(retryUrl("/ui/js/panel-settings.js", a)));
65
70
  const loadNeeds = retryable((a) => import(retryUrl("/ui/js/panel-needs.js", a)));
66
71
  const loadDetails = retryable((a) => import(retryUrl("/ui/js/panel-details.js", a)));
72
+ const loadLog = retryable((a) => import(retryUrl("/ui/js/panel-log.js", a)));
67
73
  // The stream opens once the chat listens, so the frames sent on connect (status, open prompts, ticks) reach it too.
68
74
  // If the chat cannot load (a session that just ended), the stream still opens for the sidebar.
69
75
  const chatBoot = loadChat();
@@ -77,8 +83,12 @@ if (mode === "full") {
77
83
  panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
78
84
  // #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
79
85
  panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
86
+ // #1218: one instance's Log (/ui/log/<name>).
87
+ panels.set("log", { load: () => loadLog().then((m) => m.LogPanel) });
80
88
  // #1386 §6.3: desktop notifications for new "Needs you" items, in any panel, while this signed-in page is open.
81
89
  startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
90
+ // #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
91
+ bootPrefs();
82
92
  onNewInstance = async () => {
83
93
  closeDrawer(); // on a phone the dialog opens from the drawer: the drawer goes first
84
94
  let m;
@@ -8,7 +8,7 @@
8
8
  - The optical-size axis is fixed at its text size (`opsz=14`).
9
9
  - The font is subset to Latin, with the same range as `unicode-range` in `tokens.css`.
10
10
  - It is saved as WOFF2. About 44 KB, against 352 KB for the full variable font.
11
- - Chinese and other scripts fall back to the system fonts named in `--font-ui` (PingFang TC → Noto Sans TC → Microsoft JhengHei → the system stack).
11
+ - Chinese and other scripts fall back to the system fonts named in `--font-ui` (PingFang TC → PingFang SC → Noto Sans TC → Noto Sans SC → Noto Sans CJK TC → Noto Sans CJK SC → Microsoft JhengHei → Microsoft YaHei → the system stack).
12
12
 
13
13
  Rebuilt with fontTools 4.59 (and `brotli` for WOFF2):
14
14
 
@@ -1,16 +1,20 @@
1
1
  // #1523 N3 (§3.4, Q2 = A): the controls every page's header has, in one order on Chat, View, Details and Fleet —
2
2
  // Aa text size, shared per device: S / M / L (View also keeps Fit, its own extra choice);
3
3
  // ◔ AI usage, unless the fleet turned it off (web.usage_panel: false — said by the page itself, no probe read).
4
+ // #1673: on a wide screen it is a right rail beside the page (shown or hidden per device); ◔ toggles it. On a
5
+ // narrower one, ◔ opens the dialog.
4
6
  // The size is one attribute on the root (data-text-size); CSS turns it into one custom property the chat, Details and
5
7
  // Fleet read (#1300: no style attribute). View's terminal multiplies its fitted size by VIEW_SCALE, as its density did.
6
- import { html, useState } from "./app-html.js";
8
+ import { html, useEffect, useState } from "./app-html.js";
7
9
  import { t } from "./app-i18n.js";
8
10
  import { createStore, useStore } from "./app-store.js";
9
11
  import { Icon } from "./ui-icons.js";
10
- import { UsageDialog } from "./usage-dialog.js";
12
+ import { UsageBody, UsageDialog } from "./usage-dialog.js";
11
13
 
12
14
  export const TEXT_SIZE_KEY = "agend_text_size";
13
15
  export const VIEW_FIT_KEY = "agend_view_fit";
16
+ /** #1869: set once this device's View Fit was put back (a size chosen on another page used to turn it off). */
17
+ export const VIEW_FIT_RESTORED_KEY = "agend_view_fit_1869";
14
18
  const LEGACY_DENSITY_KEY = "agend_view_density";
15
19
  export const TEXT_SIZES = ["s", "m", "l"];
16
20
  /** View's terminal: its fitted size times this (S / M / L are View's old compact / – / comfortable). */
@@ -22,6 +26,7 @@ const write = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this pa
22
26
  /**
23
27
  * This device's choice: `agend_text_size` (+ View's Fit), else — once — from View's `agend_view_density`:
24
28
  * comfortable → L, compact → S (Fit off); fit, or nothing → M with View on Fit (View's default, as before).
29
+ * #1869: until then any page's S / M / L also turned View's Fit off, so a Fit off from before is put back once.
25
30
  */
26
31
  export function loadTextSize() {
27
32
  let size = read(TEXT_SIZE_KEY), fit = read(VIEW_FIT_KEY);
@@ -29,7 +34,11 @@ export function loadTextSize() {
29
34
  const legacy = read(LEGACY_DENSITY_KEY);
30
35
  size = legacy === "comfortable" ? "l" : legacy === "compact" ? "s" : "m";
31
36
  fit = legacy === "comfortable" || legacy === "compact" ? "0" : "1";
32
- write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, fit);
37
+ write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, fit); write(VIEW_FIT_RESTORED_KEY, "1");
38
+ }
39
+ if (read(VIEW_FIT_RESTORED_KEY) !== "1") {
40
+ if (fit === "0") { fit = "1"; write(VIEW_FIT_KEY, fit); }
41
+ write(VIEW_FIT_RESTORED_KEY, "1");
33
42
  }
34
43
  return { size, viewFit: fit !== "0" };
35
44
  }
@@ -48,15 +57,22 @@ export function initTextSize() {
48
57
  window.addEventListener("storage", (e) => { if (!e || e.key === TEXT_SIZE_KEY || e.key === VIEW_FIT_KEY || e.key === null) textSizeStore.set(loadTextSize()); });
49
58
  }
50
59
  }
51
- /** A size chosen anywhere is the size everywhere (View's Fit included: the last choice wins). */
52
- export function setTextSize(size) {
60
+ /**
61
+ * A size chosen anywhere is the size everywhere. View's Fit is left only from View's own Aa (`view`): a size chosen on
62
+ * another page keeps View on Fit (#1869).
63
+ */
64
+ export function setTextSize(size, { view = false } = {}) {
53
65
  if (!TEXT_SIZES.includes(size)) return;
54
- write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, "0");
55
- textSizeStore.set({ size, viewFit: false });
66
+ write(TEXT_SIZE_KEY, size);
67
+ if (view) write(VIEW_FIT_KEY, "0");
68
+ textSizeStore.set((s) => ({ size, viewFit: view ? false : s.viewFit }));
56
69
  }
57
70
  export function setViewFit() { write(VIEW_FIT_KEY, "1"); textSizeStore.set((s) => ({ ...s, viewFit: true })); }
58
71
 
59
- /** Aa: S → M → L → S; on View, Fit → S → M → L → Fit. */
72
+ /**
73
+ * Aa: S → M → L → S; on View, Fit → S → M → L → Fit. Every page's button is as wide as its widest name (Fit's
74
+ * included): switching pages or sizes never moves the controls beside it.
75
+ */
60
76
  export function TextSizeButton({ view = false }) {
61
77
  const st = useStore(textSizeStore);
62
78
  const cur = view && st.viewFit ? "fit" : st.size;
@@ -64,8 +80,9 @@ export function TextSizeButton({ view = false }) {
64
80
  const next = order[(order.indexOf(cur) + 1) % order.length];
65
81
  const name = t(`app.textSize_${cur}`);
66
82
  const label = t("app.textSize", name);
67
- return html`<button type="button" class="btn btn-ghost btn-sm hd-text" onClick=${() => (next === "fit" ? setViewFit() : setTextSize(next))}
68
- title=${label} aria-label=${label}><${Icon} name="type" size=${16} /><span class="hide-narrow" aria-hidden="true">${name}</span></button>`;
83
+ return html`<button type="button" class="btn btn-ghost btn-sm hd-text" onClick=${() => (next === "fit" ? setViewFit() : setTextSize(next, { view }))}
84
+ title=${label} aria-label=${label}><${Icon} name="type" size=${16} /><span class="hide-narrow" aria-hidden="true"><span class="hd-text-names">${
85
+ ["fit", ...TEXT_SIZES].map(k => html`<span key=${k} class=${k === cur ? "on" : undefined}>${t(`app.textSize_${k}`)}</span>`)}</span></span></button>`;
69
86
  }
70
87
 
71
88
  /** Whether the fleet serves the usage panel: the page says so (data-usage-panel); "0" only when turned off. */
@@ -73,16 +90,65 @@ export function usageOffered() {
73
90
  try { return document.body && document.body.dataset ? document.body.dataset.usagePanel !== "0" : true; } catch { return true; }
74
91
  }
75
92
 
93
+ // #1673: the rail. Wide enough for it beside the sidebar and the page: RAIL_MEDIA (followed as it changes). Shown or
94
+ // hidden is this device's choice (agend_usage_rail; "1" = shown, "0" = hidden) — a layout choice, not synced (#1665).
95
+ // #1854 (rank 7): a device that never chose starts with it hidden — a quiet first page; ◔ shows it (and remembers).
96
+ export const RAIL_MEDIA = "(min-width: 1200px)";
97
+ export const RAIL_KEY = "agend_usage_rail";
98
+ /** The pages whose header has ◔ — the rail is theirs (Log's too: one frame on every tab of an instance). */
99
+ export const RAIL_PANELS = ["chat", "view", "log", "details", "fleet"];
100
+ export const usageRailStore = createStore({ open: false });
101
+ /** A page load: this device's choice; none made yet = hidden. */
102
+ export function initUsageRail() { usageRailStore.set({ open: read(RAIL_KEY) === "1" }); }
103
+ export function setUsageRail(open) { write(RAIL_KEY, open ? "1" : "0"); usageRailStore.set({ open: !!open }); }
104
+ const railQuery = () => { try { return typeof matchMedia === "function" ? matchMedia(RAIL_MEDIA) : null; } catch { return null; } };
105
+ /** Whether the screen is wide enough for the rail, now and as it changes. */
106
+ export function useRailWidth() {
107
+ const [wide, setWide] = useState(() => !!railQuery()?.matches);
108
+ useEffect(() => {
109
+ const q = railQuery();
110
+ if (!q) return undefined;
111
+ const on = () => setWide(!!q.matches);
112
+ on();
113
+ if (typeof q.addEventListener === "function") { q.addEventListener("change", on); return () => q.removeEventListener("change", on); }
114
+ if (typeof q.addListener === "function") { q.addListener(on); return () => q.removeListener(on); }
115
+ return undefined;
116
+ }, []);
117
+ return wide;
118
+ }
119
+
120
+ /**
121
+ * The rail: beside the page on a wide screen, on the pages with ◔, while this device shows it. Its reads are its own
122
+ * lease: they go on across navigation and end when it is hidden, the screen narrows, or it goes.
123
+ */
124
+ export function UsageRail({ route }) {
125
+ const wide = useRailWidth();
126
+ const { open } = useStore(usageRailStore);
127
+ if (!wide || !open || !route || !RAIL_PANELS.includes(route.panel) || !usageOffered()) return null;
128
+ return html`<aside class="u-rail" aria-label=${t("view.usage.title_plain")}>
129
+ <div class="u-rail-head"><h2>${t("view.usage.title")}</h2>
130
+ <button type="button" class="icon-btn" onClick=${() => setUsageRail(false)} aria-label=${t("app.usageHide")} title=${t("app.usageHide")}><${Icon} name="close" size=${16} /></button></div>
131
+ <div class="u-rail-body"><${UsageBody} owner="usage-rail" /></div>
132
+ </aside>`;
133
+ }
134
+
76
135
  /**
77
- * ◔: the usage dialog (usage-dialog.js). It belongs to the navigation that opened it (`navKey`, #1408 §4; #1563
136
+ * ◔: on a wide screen, shows or hides the rail (#1673); otherwise the usage dialog (usage-dialog.js). It belongs to the navigation that opened it (`navKey`, #1408 §4; #1563
78
137
  * review): another route, the same route again, another tab of the panel or a language switch closes it, and its
79
138
  * reads and timer end with it — the dialog's lease is that navigation's.
80
139
  */
81
140
  export function UsageButton({ navKey }) {
82
141
  const [openFor, setOpenFor] = useState(null);
142
+ const wide = useRailWidth();
143
+ const rail = useStore(usageRailStore);
144
+ // Widening ends a dialog opened on the narrower screen — closed for good, not hidden while wide: narrowing again
145
+ // (after the rail was hidden, or with this device's rail hidden) never brings it back (#1679 review).
146
+ useEffect(() => { if (wide) setOpenFor(null); }, [wide]);
83
147
  if (!usageOffered()) return null;
84
- const open = openFor !== null && openFor === navKey;
85
- return html`<button type="button" class="btn btn-ghost btn-sm hd-usage" onClick=${() => setOpenFor(navKey)} aria-label=${t("app.usage")} title=${t("app.usage")}>
148
+ const open = !wide && openFor !== null && openFor === navKey;
149
+ const click = () => (wide ? setUsageRail(!rail.open) : setOpenFor(navKey));
150
+ return html`<button type="button" class="btn btn-ghost btn-sm hd-usage" onClick=${click} aria-label=${t("app.usage")} title=${t("app.usage")}
151
+ aria-pressed=${wide ? (rail.open ? "true" : "false") : undefined}>
86
152
  <${Icon} name="chart" size=${16} /><span class="hide-narrow" aria-hidden="true">${t("app.usage")}</span></button>
87
153
  ${open ? html`<${UsageDialog} key=${navKey} owner=${navKey} onClose=${() => setOpenFor(null)} />` : null}`;
88
154
  }
@@ -91,3 +157,7 @@ export function UsageButton({ navKey }) {
91
157
  export function HeaderTools({ view = false, navKey }) {
92
158
  return html`<${TextSizeButton} view=${view} /><${UsageButton} navKey=${navKey} />`;
93
159
  }
160
+ /** Where a page with no control of its own after the shared ones (Log) keeps the room Chat's ⋯ takes: the same box on every tab. */
161
+ export function HeaderSlot() {
162
+ return html`<span class="icon-btn hd-slot" aria-hidden="true"></span>`;
163
+ }