openclaw-multi-auto 1.3.6 → 1.3.8

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 (328) hide show
  1. package/dist/{audio-preflight-5FEeDooz.js → audio-preflight-DDBLZBdb.js} +4 -4
  2. package/dist/{audio-transcription-runner-B-UvoDjZ.js → audio-transcription-runner-DZbSWT9E.js} +1 -1
  3. package/dist/build-info.json +3 -3
  4. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  5. package/dist/{chrome-D45SyhQL.js → chrome-CMU2WVFh.js} +8 -8
  6. package/dist/{deliver-B9cys0EZ.js → deliver-BXVcFIHL.js} +1 -1
  7. package/dist/{deliver-runtime-DhaQJ0pI.js → deliver-runtime-DTaIS-1i.js} +3 -3
  8. package/dist/{deps-send-whatsapp.runtime-DvTL2tzN.js → deps-send-whatsapp.runtime-CIZqFAqb.js} +7 -7
  9. package/dist/extensionAPI.js +6 -6
  10. package/dist/{image-DAOPwVXi.js → image-BCVLo0qw.js} +1 -1
  11. package/dist/{image-runtime-wlCLVvVv.js → image-runtime-DtCKpMPZ.js} +3 -3
  12. package/dist/{pi-embedded-DYU79yGe.js → pi-embedded-CgQ_W6Xs.js} +24 -24
  13. package/dist/{pi-embedded-helpers-uTRAmQ4n.js → pi-embedded-helpers-CwuBTKza.js} +3 -3
  14. package/dist/plugin-sdk/{accounts-DyFCXtHv.js → accounts-BslAlVYS.js} +2 -2
  15. package/dist/plugin-sdk/{accounts-BJAXxY46.js → accounts-C3m65--E.js} +2 -2
  16. package/dist/plugin-sdk/{accounts-C1j7HSL0.js → accounts-CNCCkdEF.js} +3 -3
  17. package/dist/plugin-sdk/{active-listener-CftX5jLD.js → active-listener-CkPnMUkB.js} +2 -2
  18. package/dist/plugin-sdk/{api-key-rotation-8nyyt1kx.js → api-key-rotation-BXnNsojA.js} +2 -2
  19. package/dist/plugin-sdk/{audio-preflight-C_aSAPR1.js → audio-preflight-CtO4fFvp.js} +26 -26
  20. package/dist/plugin-sdk/{audio-transcription-runner-CB53F7_7.js → audio-transcription-runner-DnxvOS1-.js} +11 -11
  21. package/dist/plugin-sdk/{audit-membership-runtime-BXndI4LG.js → audit-membership-runtime-BpfoSk8M.js} +2 -2
  22. package/dist/plugin-sdk/{channel-activity-C5y8AgAV.js → channel-activity-WJYxcJ3S.js} +3 -3
  23. package/dist/plugin-sdk/{channel-web-DBTRO03V.js → channel-web-dO5k3ubM.js} +18 -18
  24. package/dist/plugin-sdk/{chrome-f00sZkDX.js → chrome-CjNTuJML.js} +6 -6
  25. package/dist/plugin-sdk/{commands-registry-BJ_NxG2F.js → commands-registry-CdYjoI0i.js} +4 -4
  26. package/dist/plugin-sdk/{common-Cf27Jwxu.js → common-oYc5vPFl.js} +2 -2
  27. package/dist/plugin-sdk/{config-CHQrpx-Q.js → config-B1z-UxQ3.js} +7 -7
  28. package/dist/plugin-sdk/{deliver-DNEuetST.js → deliver-D5_6T567.js} +10 -10
  29. package/dist/plugin-sdk/deliver-runtime-C5dgvvga.js +32 -0
  30. package/dist/plugin-sdk/deps-send-discord.runtime-Dg4N7PHJ.js +23 -0
  31. package/dist/plugin-sdk/deps-send-imessage.runtime-0OEwzMQm.js +22 -0
  32. package/dist/plugin-sdk/deps-send-signal.runtime-BM1jRt3G.js +21 -0
  33. package/dist/plugin-sdk/deps-send-slack.runtime-1E3BYRdF.js +19 -0
  34. package/dist/plugin-sdk/deps-send-telegram.runtime-DNCxIflA.js +24 -0
  35. package/dist/plugin-sdk/deps-send-whatsapp.runtime-OLwr-9c8.js +57 -0
  36. package/dist/plugin-sdk/{diagnostic-LYUUmjJ5.js → diagnostic-Bxxu0ig-.js} +2 -2
  37. package/dist/plugin-sdk/{errors-CtMWwS2Z.js → errors-B3cHyZZA.js} +1 -1
  38. package/dist/plugin-sdk/{fetch-guard-CxYB5Kg6.js → fetch-guard-Dcgod0tg.js} +2 -2
  39. package/dist/plugin-sdk/{fs-safe-DtfhxbrI.js → fs-safe-BaKqI3G4.js} +3 -3
  40. package/dist/plugin-sdk/{image-BwjYjRHx.js → image-B2mQW9Rb.js} +6 -6
  41. package/dist/plugin-sdk/{image-ops-BnZKcbd6.js → image-ops-Cbzr4U9l.js} +2 -2
  42. package/dist/plugin-sdk/image-runtime-BFm45j49.js +25 -0
  43. package/dist/plugin-sdk/{ir-Z4hX67TJ.js → ir-ZEmrTr4J.js} +7 -7
  44. package/dist/plugin-sdk/{local-roots-KhjQw04O.js → local-roots-CIPRxA-4.js} +4 -4
  45. package/dist/plugin-sdk/{logger-DHIIvMxj.js → logger-CvPFVOgT.js} +2 -2
  46. package/dist/plugin-sdk/{login-C31642Ld.js → login-CCTew9bt.js} +4 -4
  47. package/dist/plugin-sdk/{login-qr--y2SG_Ue.js → login-qr-BI3Vi_wJ.js} +5 -5
  48. package/dist/plugin-sdk/{manager-2UZBMCc7.js → manager-BEoYPn7R.js} +8 -8
  49. package/dist/plugin-sdk/manager-runtime-DxclHQ4U.js +15 -0
  50. package/dist/plugin-sdk/{outbound-Ba0QUI5h.js → outbound-ByOw1K6W.js} +5 -5
  51. package/dist/plugin-sdk/{outbound-attachment-B1Laso-8.js → outbound-attachment-BzVhxRRw.js} +2 -2
  52. package/dist/plugin-sdk/{path-alias-guards-C7Vm5DZ1.js → path-alias-guards-sWayacde.js} +1 -1
  53. package/dist/plugin-sdk/{paths-DopV9PQG.js → paths-Dpg3qxcl.js} +1 -1
  54. package/dist/plugin-sdk/{pi-embedded-helpers-DnA_OCzP.js → pi-embedded-helpers-DIxXkGJf.js} +16 -16
  55. package/dist/plugin-sdk/{pi-model-discovery-DdPqXk8f.js → pi-model-discovery-DM_2uFtj.js} +1 -1
  56. package/dist/plugin-sdk/pi-model-discovery-runtime-BuzvkvNR.js +8 -0
  57. package/dist/plugin-sdk/{pi-tools.before-tool-call.runtime-DxFHiLUE.js → pi-tools.before-tool-call.runtime-w1dqL_ty.js} +4 -4
  58. package/dist/plugin-sdk/{plugins-CbCt4osF.js → plugins-C4USiH29.js} +4 -4
  59. package/dist/plugin-sdk/{proxy-env-C63mMdas.js → proxy-env-ET-rp8eg.js} +1 -1
  60. package/dist/plugin-sdk/{proxy-fetch-Ch95c_Y2.js → proxy-fetch-uDXGKG3Z.js} +1 -1
  61. package/dist/plugin-sdk/{pw-ai-DpJk62D4.js → pw-ai-CyOt3RDA.js} +9 -9
  62. package/dist/plugin-sdk/{qmd-manager-Ca-iSfEE.js → qmd-manager-BySdoVR7.js} +7 -7
  63. package/dist/plugin-sdk/{query-expansion-B_Xe41Ab.js → query-expansion-C6uS-7lj.js} +4 -4
  64. package/dist/plugin-sdk/{redact-hp9TOulW.js → redact-Bvxt1T_Q.js} +1 -1
  65. package/dist/plugin-sdk/{reply-CovBlFea.js → reply-CTCSeQqW.js} +73 -73
  66. package/dist/plugin-sdk/{resolve-outbound-target-BbrHgyUk.js → resolve-outbound-target-Bw8YNANu.js} +2 -2
  67. package/dist/plugin-sdk/{run-with-concurrency-BR1DXa8T.js → run-with-concurrency-C_KCHwvf.js} +1 -1
  68. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-BxgRDkhc.js +10 -0
  69. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-elOqrkfg.js +19 -0
  70. package/dist/plugin-sdk/{send-BvAtLLPl.js → send-BZ6nYFZr.js} +5 -5
  71. package/dist/plugin-sdk/{send-BTztm3D2.js → send-C0w6xP2x.js} +6 -6
  72. package/dist/plugin-sdk/{send-CWJUuG0i.js → send-CFf-1V89.js} +8 -8
  73. package/dist/plugin-sdk/{send-EcglC4cG.js → send-CY-Qfwia.js} +7 -7
  74. package/dist/plugin-sdk/{send-BXpXBwM_.js → send-qPyNGSe4.js} +13 -13
  75. package/dist/plugin-sdk/{session-k256LJZT.js → session-COrvpvUQ.js} +3 -3
  76. package/dist/plugin-sdk/signal.js +2 -2
  77. package/dist/plugin-sdk/{skill-commands-DoRqLzxm.js → skill-commands-DZqhtmiv.js} +4 -4
  78. package/dist/plugin-sdk/{skills-QudILG6e.js → skills-Cw_vXEJb.js} +6 -6
  79. package/dist/plugin-sdk/slash-commands.runtime-D67JLweo.js +13 -0
  80. package/dist/plugin-sdk/slash-dispatch.runtime-DvcpvCJ0.js +52 -0
  81. package/dist/plugin-sdk/slash-skill-commands.runtime-BM1x3azR.js +16 -0
  82. package/dist/plugin-sdk/{store-BbDQw3g6.js → store-CMHj6IIw.js} +2 -2
  83. package/dist/plugin-sdk/subagent-registry-runtime-1lbDyRzz.js +52 -0
  84. package/dist/plugin-sdk/{tables-BhvloMKN.js → tables-CSqrHsKL.js} +1 -1
  85. package/dist/plugin-sdk/{thinking-URzkT-3p.js → thinking-DOnsR_A8.js} +7 -7
  86. package/dist/plugin-sdk/{tokens-B1PW5Ayy.js → tokens-BDr0Z9o3.js} +1 -1
  87. package/dist/plugin-sdk/{tool-images-xpqbP6RR.js → tool-images-eEfOVkzf.js} +2 -2
  88. package/dist/plugin-sdk/web-BLyT64pW.js +56 -0
  89. package/dist/plugin-sdk/{whatsapp-actions-RcZ6vp61.js → whatsapp-actions-xcleMoMv.js} +17 -17
  90. package/dist/plugin-sdk/whatsapp.js +50 -50
  91. package/dist/{pw-ai-GcYO6HPE.js → pw-ai-CmphSzHx.js} +1 -1
  92. package/dist/{slash-dispatch.runtime-Dh053pQK.js → slash-dispatch.runtime-131yup2e.js} +6 -6
  93. package/dist/{subagent-registry-runtime-DSi5mnCQ.js → subagent-registry-runtime-DbSf_Je6.js} +6 -6
  94. package/dist/{web-1hWJDzNA.js → web-MR9d7KyB.js} +6 -6
  95. package/package.json +5 -2
  96. package/scripts/create-instance.sh +44 -19
  97. package/scripts/install-maca.sh +39 -28
  98. package/scripts/npm_publish.sh +8 -6
  99. package/ui/index.html +16 -0
  100. package/ui/node_modules/.bin/jiti +21 -0
  101. package/ui/node_modules/.bin/lessc +21 -0
  102. package/ui/node_modules/.bin/marked +21 -0
  103. package/ui/node_modules/.bin/playwright +21 -0
  104. package/ui/node_modules/.bin/sass +21 -0
  105. package/ui/node_modules/.bin/tsx +21 -0
  106. package/ui/node_modules/.bin/vite +21 -0
  107. package/ui/node_modules/.bin/vitest +21 -0
  108. package/ui/node_modules/.bin/yaml +21 -0
  109. package/ui/package.json +27 -0
  110. package/ui/public/apple-touch-icon.png +0 -0
  111. package/ui/public/favicon-32.png +0 -0
  112. package/ui/public/favicon.ico +0 -0
  113. package/ui/public/favicon.svg +22 -0
  114. package/ui/src/css.d.ts +1 -0
  115. package/ui/src/i18n/index.ts +3 -0
  116. package/ui/src/i18n/lib/lit-controller.ts +22 -0
  117. package/ui/src/i18n/lib/registry.ts +64 -0
  118. package/ui/src/i18n/lib/translate.ts +123 -0
  119. package/ui/src/i18n/lib/types.ts +9 -0
  120. package/ui/src/i18n/locales/de.ts +129 -0
  121. package/ui/src/i18n/locales/en.ts +337 -0
  122. package/ui/src/i18n/locales/pt-BR.ts +128 -0
  123. package/ui/src/i18n/locales/zh-CN.ts +330 -0
  124. package/ui/src/i18n/locales/zh-TW.ts +125 -0
  125. package/ui/src/i18n/test/translate.test.ts +56 -0
  126. package/ui/src/main.ts +2 -0
  127. package/ui/src/styles/base.css +385 -0
  128. package/ui/src/styles/chat/grouped.css +300 -0
  129. package/ui/src/styles/chat/layout.css +481 -0
  130. package/ui/src/styles/chat/sidebar.css +117 -0
  131. package/ui/src/styles/chat/text.css +146 -0
  132. package/ui/src/styles/chat/tool-cards.css +202 -0
  133. package/ui/src/styles/chat.css +5 -0
  134. package/ui/src/styles/components.css +2612 -0
  135. package/ui/src/styles/config.css +1658 -0
  136. package/ui/src/styles/layout.css +621 -0
  137. package/ui/src/styles/layout.mobile.css +374 -0
  138. package/ui/src/styles.css +5 -0
  139. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-flags-unsupported-unions-1.png +0 -0
  140. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-renders-inputs-and-patches-values-1.png +0 -0
  141. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-renders-union-literals-as-select-options-1.png +0 -0
  142. package/ui/src/ui/__screenshots__/navigation.browser.test.ts/control-UI-routing-auto-scrolls-chat-history-to-the-latest-message-1.png +0 -0
  143. package/ui/src/ui/app-channels.ts +279 -0
  144. package/ui/src/ui/app-chat.ts +266 -0
  145. package/ui/src/ui/app-defaults.ts +50 -0
  146. package/ui/src/ui/app-events.ts +5 -0
  147. package/ui/src/ui/app-gateway.node.test.ts +229 -0
  148. package/ui/src/ui/app-gateway.ts +349 -0
  149. package/ui/src/ui/app-lifecycle.node.test.ts +44 -0
  150. package/ui/src/ui/app-lifecycle.ts +109 -0
  151. package/ui/src/ui/app-polling.ts +69 -0
  152. package/ui/src/ui/app-render-usage-tab.ts +273 -0
  153. package/ui/src/ui/app-render.helpers.node.test.ts +286 -0
  154. package/ui/src/ui/app-render.helpers.ts +574 -0
  155. package/ui/src/ui/app-render.ts +1168 -0
  156. package/ui/src/ui/app-scroll.test.ts +275 -0
  157. package/ui/src/ui/app-scroll.ts +179 -0
  158. package/ui/src/ui/app-settings.test.ts +70 -0
  159. package/ui/src/ui/app-settings.ts +440 -0
  160. package/ui/src/ui/app-tool-stream.node.test.ts +139 -0
  161. package/ui/src/ui/app-tool-stream.ts +455 -0
  162. package/ui/src/ui/app-view-state.ts +321 -0
  163. package/ui/src/ui/app.ts +621 -0
  164. package/ui/src/ui/assistant-identity.ts +23 -0
  165. package/ui/src/ui/chat/constants.ts +12 -0
  166. package/ui/src/ui/chat/copy-as-markdown.ts +97 -0
  167. package/ui/src/ui/chat/grouped-render.ts +287 -0
  168. package/ui/src/ui/chat/message-extract.test.ts +64 -0
  169. package/ui/src/ui/chat/message-extract.ts +122 -0
  170. package/ui/src/ui/chat/message-normalizer.test.ts +179 -0
  171. package/ui/src/ui/chat/message-normalizer.ts +101 -0
  172. package/ui/src/ui/chat/tool-cards.ts +156 -0
  173. package/ui/src/ui/chat/tool-helpers.test.ts +141 -0
  174. package/ui/src/ui/chat/tool-helpers.ts +37 -0
  175. package/ui/src/ui/chat-event-reload.test.ts +47 -0
  176. package/ui/src/ui/chat-event-reload.ts +16 -0
  177. package/ui/src/ui/chat-markdown.browser.test.ts +37 -0
  178. package/ui/src/ui/components/resizable-divider.ts +110 -0
  179. package/ui/src/ui/config-form.browser.test.ts +443 -0
  180. package/ui/src/ui/controllers/agent-files.ts +126 -0
  181. package/ui/src/ui/controllers/agent-identity.ts +59 -0
  182. package/ui/src/ui/controllers/agent-skills.ts +33 -0
  183. package/ui/src/ui/controllers/agents.test.ts +61 -0
  184. package/ui/src/ui/controllers/agents.ts +64 -0
  185. package/ui/src/ui/controllers/assistant-identity.ts +34 -0
  186. package/ui/src/ui/controllers/channels.ts +94 -0
  187. package/ui/src/ui/controllers/channels.types.ts +15 -0
  188. package/ui/src/ui/controllers/chat.test.ts +568 -0
  189. package/ui/src/ui/controllers/chat.ts +318 -0
  190. package/ui/src/ui/controllers/config/form-coerce.ts +160 -0
  191. package/ui/src/ui/controllers/config/form-utils.node.test.ts +455 -0
  192. package/ui/src/ui/controllers/config/form-utils.ts +90 -0
  193. package/ui/src/ui/controllers/config.test.ts +289 -0
  194. package/ui/src/ui/controllers/config.ts +219 -0
  195. package/ui/src/ui/controllers/control-ui-bootstrap.test.ts +82 -0
  196. package/ui/src/ui/controllers/control-ui-bootstrap.ts +49 -0
  197. package/ui/src/ui/controllers/cron-filters.test.ts +81 -0
  198. package/ui/src/ui/controllers/cron.test.ts +1070 -0
  199. package/ui/src/ui/controllers/cron.ts +921 -0
  200. package/ui/src/ui/controllers/debug.ts +60 -0
  201. package/ui/src/ui/controllers/devices.ts +159 -0
  202. package/ui/src/ui/controllers/exec-approval.ts +100 -0
  203. package/ui/src/ui/controllers/exec-approvals.ts +170 -0
  204. package/ui/src/ui/controllers/logs.ts +147 -0
  205. package/ui/src/ui/controllers/nodes.ts +32 -0
  206. package/ui/src/ui/controllers/presence.ts +37 -0
  207. package/ui/src/ui/controllers/sessions.test.ts +104 -0
  208. package/ui/src/ui/controllers/sessions.ts +127 -0
  209. package/ui/src/ui/controllers/skills.ts +157 -0
  210. package/ui/src/ui/controllers/usage.node.test.ts +181 -0
  211. package/ui/src/ui/controllers/usage.ts +315 -0
  212. package/ui/src/ui/data/moonshot-kimi-k2.ts +45 -0
  213. package/ui/src/ui/device-auth.ts +73 -0
  214. package/ui/src/ui/device-identity.ts +112 -0
  215. package/ui/src/ui/external-link.test.ts +18 -0
  216. package/ui/src/ui/external-link.ts +19 -0
  217. package/ui/src/ui/focus-mode.browser.test.ts +39 -0
  218. package/ui/src/ui/format.test.ts +101 -0
  219. package/ui/src/ui/format.ts +60 -0
  220. package/ui/src/ui/gateway.ts +360 -0
  221. package/ui/src/ui/icons.ts +256 -0
  222. package/ui/src/ui/markdown.test.ts +85 -0
  223. package/ui/src/ui/markdown.ts +139 -0
  224. package/ui/src/ui/navigation.browser.test.ts +188 -0
  225. package/ui/src/ui/navigation.test.ts +189 -0
  226. package/ui/src/ui/navigation.ts +165 -0
  227. package/ui/src/ui/open-external-url.test.ts +108 -0
  228. package/ui/src/ui/open-external-url.ts +73 -0
  229. package/ui/src/ui/presenter.ts +85 -0
  230. package/ui/src/ui/storage.node.test.ts +63 -0
  231. package/ui/src/ui/storage.ts +99 -0
  232. package/ui/src/ui/test-helpers/app-mount.ts +27 -0
  233. package/ui/src/ui/text-direction.test.ts +24 -0
  234. package/ui/src/ui/text-direction.ts +30 -0
  235. package/ui/src/ui/theme-transition.ts +109 -0
  236. package/ui/src/ui/theme.ts +16 -0
  237. package/ui/src/ui/tool-display.ts +159 -0
  238. package/ui/src/ui/types/chat-types.ts +44 -0
  239. package/ui/src/ui/types.ts +627 -0
  240. package/ui/src/ui/ui-types.ts +54 -0
  241. package/ui/src/ui/usage-helpers.node.test.ts +43 -0
  242. package/ui/src/ui/usage-helpers.ts +321 -0
  243. package/ui/src/ui/usage-types.ts +22 -0
  244. package/ui/src/ui/uuid.test.ts +41 -0
  245. package/ui/src/ui/uuid.ts +57 -0
  246. package/ui/src/ui/views/agents-panels-status-files.ts +461 -0
  247. package/ui/src/ui/views/agents-panels-tools-skills.browser.test.ts +102 -0
  248. package/ui/src/ui/views/agents-panels-tools-skills.ts +537 -0
  249. package/ui/src/ui/views/agents-utils.test.ts +100 -0
  250. package/ui/src/ui/views/agents-utils.ts +502 -0
  251. package/ui/src/ui/views/agents.ts +499 -0
  252. package/ui/src/ui/views/channel-config-extras.ts +49 -0
  253. package/ui/src/ui/views/channels.config.ts +155 -0
  254. package/ui/src/ui/views/channels.discord.ts +65 -0
  255. package/ui/src/ui/views/channels.googlechat.ts +79 -0
  256. package/ui/src/ui/views/channels.imessage.ts +65 -0
  257. package/ui/src/ui/views/channels.nostr-profile-form.ts +321 -0
  258. package/ui/src/ui/views/channels.nostr.ts +237 -0
  259. package/ui/src/ui/views/channels.shared.ts +38 -0
  260. package/ui/src/ui/views/channels.signal.ts +69 -0
  261. package/ui/src/ui/views/channels.slack.ts +65 -0
  262. package/ui/src/ui/views/channels.telegram.ts +120 -0
  263. package/ui/src/ui/views/channels.ts +325 -0
  264. package/ui/src/ui/views/channels.types.ts +62 -0
  265. package/ui/src/ui/views/channels.whatsapp.ts +118 -0
  266. package/ui/src/ui/views/chat-image-open.browser.test.ts +70 -0
  267. package/ui/src/ui/views/chat.test.ts +227 -0
  268. package/ui/src/ui/views/chat.ts +616 -0
  269. package/ui/src/ui/views/config-form.analyze.ts +267 -0
  270. package/ui/src/ui/views/config-form.node.ts +1073 -0
  271. package/ui/src/ui/views/config-form.render.ts +478 -0
  272. package/ui/src/ui/views/config-form.search.node.test.ts +69 -0
  273. package/ui/src/ui/views/config-form.shared.ts +96 -0
  274. package/ui/src/ui/views/config-form.ts +4 -0
  275. package/ui/src/ui/views/config-search.node.test.ts +50 -0
  276. package/ui/src/ui/views/config-search.ts +92 -0
  277. package/ui/src/ui/views/config.browser.test.ts +233 -0
  278. package/ui/src/ui/views/config.ts +820 -0
  279. package/ui/src/ui/views/cron.test.ts +741 -0
  280. package/ui/src/ui/views/cron.ts +1758 -0
  281. package/ui/src/ui/views/debug.ts +151 -0
  282. package/ui/src/ui/views/exec-approval.ts +89 -0
  283. package/ui/src/ui/views/gateway-url-confirmation.ts +40 -0
  284. package/ui/src/ui/views/instances.ts +89 -0
  285. package/ui/src/ui/views/logs.ts +155 -0
  286. package/ui/src/ui/views/markdown-sidebar.ts +40 -0
  287. package/ui/src/ui/views/nodes-exec-approvals.ts +617 -0
  288. package/ui/src/ui/views/nodes-shared.ts +67 -0
  289. package/ui/src/ui/views/nodes.ts +485 -0
  290. package/ui/src/ui/views/overview-hints.ts +16 -0
  291. package/ui/src/ui/views/overview.node.test.ts +39 -0
  292. package/ui/src/ui/views/overview.ts +361 -0
  293. package/ui/src/ui/views/sessions.test.ts +81 -0
  294. package/ui/src/ui/views/sessions.ts +321 -0
  295. package/ui/src/ui/views/skills-grouping.ts +40 -0
  296. package/ui/src/ui/views/skills-shared.ts +52 -0
  297. package/ui/src/ui/views/skills.ts +192 -0
  298. package/ui/src/ui/views/usage-metrics.ts +578 -0
  299. package/ui/src/ui/views/usage-query.ts +277 -0
  300. package/ui/src/ui/views/usage-render-details.test.ts +136 -0
  301. package/ui/src/ui/views/usage-render-details.ts +1083 -0
  302. package/ui/src/ui/views/usage-render-overview.ts +796 -0
  303. package/ui/src/ui/views/usage-styles/usageStyles-part1.ts +701 -0
  304. package/ui/src/ui/views/usage-styles/usageStyles-part2.ts +702 -0
  305. package/ui/src/ui/views/usage-styles/usageStyles-part3.ts +551 -0
  306. package/ui/src/ui/views/usage.ts +836 -0
  307. package/ui/src/ui/views/usageStyles.ts +5 -0
  308. package/ui/src/ui/views/usageTypes.ts +105 -0
  309. package/ui/vite.config.ts +43 -0
  310. package/ui/vitest.config.ts +15 -0
  311. package/ui/vitest.node.config.ts +10 -0
  312. package/dist/plugin-sdk/deliver-runtime-BFdqklJM.js +0 -32
  313. package/dist/plugin-sdk/deps-send-discord.runtime-DuqpYwU0.js +0 -23
  314. package/dist/plugin-sdk/deps-send-imessage.runtime-CZ2rS8Lb.js +0 -22
  315. package/dist/plugin-sdk/deps-send-signal.runtime-BdqiWhIh.js +0 -21
  316. package/dist/plugin-sdk/deps-send-slack.runtime-04s36qiC.js +0 -19
  317. package/dist/plugin-sdk/deps-send-telegram.runtime-LE5tkPvr.js +0 -24
  318. package/dist/plugin-sdk/deps-send-whatsapp.runtime-Bz57lobC.js +0 -57
  319. package/dist/plugin-sdk/image-runtime-B8twoubs.js +0 -25
  320. package/dist/plugin-sdk/manager-runtime-CMeLwose.js +0 -15
  321. package/dist/plugin-sdk/pi-model-discovery-runtime-D8CJhtJY.js +0 -8
  322. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-SkO91TZH.js +0 -10
  323. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-B0VWK5hm.js +0 -19
  324. package/dist/plugin-sdk/slash-commands.runtime-DS6vCNSL.js +0 -13
  325. package/dist/plugin-sdk/slash-dispatch.runtime-BXrxb2wd.js +0 -52
  326. package/dist/plugin-sdk/slash-skill-commands.runtime-Bd6qQ2oT.js +0 -16
  327. package/dist/plugin-sdk/subagent-registry-runtime-1uwQbuXj.js +0 -52
  328. package/dist/plugin-sdk/web-B74yhL2N.js +0 -56
@@ -0,0 +1,1083 @@
1
+ import { html, svg, nothing } from "lit";
2
+ import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
3
+ import { parseToolSummary } from "../usage-helpers.ts";
4
+ import { charsToTokens, formatCost, formatTokens } from "./usage-metrics.ts";
5
+ import { renderInsightList } from "./usage-render-overview.ts";
6
+ import {
7
+ SessionLogEntry,
8
+ SessionLogRole,
9
+ TimeSeriesPoint,
10
+ UsageSessionEntry,
11
+ } from "./usageTypes.ts";
12
+
13
+ // Chart constants
14
+ const CHART_BAR_WIDTH_RATIO = 0.75; // Fraction of slot used for bar (rest is gap)
15
+ const CHART_MAX_BAR_WIDTH = 8; // Max bar width in SVG viewBox units
16
+ const CHART_SELECTION_OPACITY = 0.06; // Opacity of range selection overlay
17
+ const HANDLE_WIDTH = 5; // Width of drag handle in SVG units
18
+ const HANDLE_HEIGHT = 12; // Height of drag handle
19
+ const HANDLE_GRIP_OFFSET = 0.7; // Offset of grip lines inside handle
20
+
21
+ function pct(part: number, total: number): number {
22
+ if (!total || total <= 0) {
23
+ return 0;
24
+ }
25
+ return (part / total) * 100;
26
+ }
27
+
28
+ function renderEmptyDetailState() {
29
+ return nothing;
30
+ }
31
+
32
+ /** Normalize a log timestamp to milliseconds (handles seconds vs ms). */
33
+ function normalizeLogTimestamp(ts: number): number {
34
+ return ts < 1e12 ? ts * 1000 : ts;
35
+ }
36
+
37
+ /** Filter session logs by a timestamp range. */
38
+ function filterLogsByRange(
39
+ logs: SessionLogEntry[],
40
+ rangeStart: number,
41
+ rangeEnd: number,
42
+ ): SessionLogEntry[] {
43
+ const lo = Math.min(rangeStart, rangeEnd);
44
+ const hi = Math.max(rangeStart, rangeEnd);
45
+ return logs.filter((log) => {
46
+ if (log.timestamp <= 0) {
47
+ return true;
48
+ }
49
+ const ts = normalizeLogTimestamp(log.timestamp);
50
+ return ts >= lo && ts <= hi;
51
+ });
52
+ }
53
+
54
+ function renderSessionSummary(
55
+ session: UsageSessionEntry,
56
+ filteredUsage?: UsageSessionEntry["usage"],
57
+ filteredLogs?: SessionLogEntry[],
58
+ ) {
59
+ const usage = filteredUsage || session.usage;
60
+ if (!usage) {
61
+ return html`
62
+ <div class="muted">No usage data for this session.</div>
63
+ `;
64
+ }
65
+
66
+ const formatTs = (ts?: number): string => (ts ? new Date(ts).toLocaleString() : "—");
67
+
68
+ const badges: string[] = [];
69
+ if (session.channel) {
70
+ badges.push(`channel:${session.channel}`);
71
+ }
72
+ if (session.agentId) {
73
+ badges.push(`agent:${session.agentId}`);
74
+ }
75
+ if (session.modelProvider || session.providerOverride) {
76
+ badges.push(`provider:${session.modelProvider ?? session.providerOverride}`);
77
+ }
78
+ if (session.model) {
79
+ badges.push(`model:${session.model}`);
80
+ }
81
+
82
+ // Always use the full tool list for stable layout; update counts when filtering
83
+ const baseTools = usage.toolUsage?.tools.slice(0, 6) ?? [];
84
+ let toolCallCount: number;
85
+ let uniqueToolCount: number;
86
+ let toolItems: Array<{ label: string; value: string; sub: string }>;
87
+
88
+ if (filteredLogs) {
89
+ const toolCounts = new Map<string, number>();
90
+ for (const log of filteredLogs) {
91
+ const { tools } = parseToolSummary(log.content);
92
+ for (const [name] of tools) {
93
+ toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
94
+ }
95
+ }
96
+ // Keep the same tool order as the full session, just update counts
97
+ toolItems = baseTools.map((tool) => ({
98
+ label: tool.name,
99
+ value: `${toolCounts.get(tool.name) ?? 0}`,
100
+ sub: "calls",
101
+ }));
102
+ toolCallCount = [...toolCounts.values()].reduce((sum, c) => sum + c, 0);
103
+ uniqueToolCount = toolCounts.size;
104
+ } else {
105
+ toolItems = baseTools.map((tool) => ({
106
+ label: tool.name,
107
+ value: `${tool.count}`,
108
+ sub: "calls",
109
+ }));
110
+ toolCallCount = usage.toolUsage?.totalCalls ?? 0;
111
+ uniqueToolCount = usage.toolUsage?.uniqueTools ?? 0;
112
+ }
113
+ const modelItems =
114
+ usage.modelUsage?.slice(0, 6).map((entry) => ({
115
+ label: entry.model ?? "unknown",
116
+ value: formatCost(entry.totals.totalCost),
117
+ sub: formatTokens(entry.totals.totalTokens),
118
+ })) ?? [];
119
+
120
+ return html`
121
+ ${badges.length > 0 ? html`<div class="usage-badges">${badges.map((b) => html`<span class="usage-badge">${b}</span>`)}</div>` : nothing}
122
+ <div class="session-summary-grid">
123
+ <div class="session-summary-card">
124
+ <div class="session-summary-title">Messages</div>
125
+ <div class="session-summary-value">${usage.messageCounts?.total ?? 0}</div>
126
+ <div class="session-summary-meta">${usage.messageCounts?.user ?? 0} user · ${usage.messageCounts?.assistant ?? 0} assistant</div>
127
+ </div>
128
+ <div class="session-summary-card">
129
+ <div class="session-summary-title">Tool Calls</div>
130
+ <div class="session-summary-value">${toolCallCount}</div>
131
+ <div class="session-summary-meta">${uniqueToolCount} tools</div>
132
+ </div>
133
+ <div class="session-summary-card">
134
+ <div class="session-summary-title">Errors</div>
135
+ <div class="session-summary-value">${usage.messageCounts?.errors ?? 0}</div>
136
+ <div class="session-summary-meta">${usage.messageCounts?.toolResults ?? 0} tool results</div>
137
+ </div>
138
+ <div class="session-summary-card">
139
+ <div class="session-summary-title">Duration</div>
140
+ <div class="session-summary-value">${formatDurationCompact(usage.durationMs, { spaced: true }) ?? "—"}</div>
141
+ <div class="session-summary-meta">${formatTs(usage.firstActivity)} → ${formatTs(usage.lastActivity)}</div>
142
+ </div>
143
+ </div>
144
+ <div class="usage-insights-grid" style="margin-top: 12px;">
145
+ ${renderInsightList("Top Tools", toolItems, "No tool calls")}
146
+ ${renderInsightList("Model Mix", modelItems, "No model data")}
147
+ </div>
148
+ `;
149
+ }
150
+
151
+ /** Aggregate usage stats from time series points within a timestamp range. */
152
+ function computeFilteredUsage(
153
+ baseUsage: NonNullable<UsageSessionEntry["usage"]>,
154
+ points: TimeSeriesPoint[],
155
+ rangeStart: number,
156
+ rangeEnd: number,
157
+ ): UsageSessionEntry["usage"] | undefined {
158
+ const lo = Math.min(rangeStart, rangeEnd);
159
+ const hi = Math.max(rangeStart, rangeEnd);
160
+ const filtered = points.filter((p) => p.timestamp >= lo && p.timestamp <= hi);
161
+ if (filtered.length === 0) {
162
+ return undefined;
163
+ }
164
+
165
+ let totalTokens = 0;
166
+ let totalCost = 0;
167
+ let userMessages = 0;
168
+ let assistantMessages = 0;
169
+ let totalInput = 0;
170
+ let totalOutput = 0;
171
+ let totalCacheRead = 0;
172
+ let totalCacheWrite = 0;
173
+
174
+ for (const p of filtered) {
175
+ totalTokens += p.totalTokens || 0;
176
+ totalCost += p.cost || 0;
177
+ totalInput += p.input || 0;
178
+ totalOutput += p.output || 0;
179
+ totalCacheRead += p.cacheRead || 0;
180
+ totalCacheWrite += p.cacheWrite || 0;
181
+ if (p.output > 0) {
182
+ assistantMessages++;
183
+ }
184
+ if (p.input > 0) {
185
+ userMessages++;
186
+ }
187
+ }
188
+
189
+ return {
190
+ ...baseUsage,
191
+ totalTokens,
192
+ totalCost,
193
+ input: totalInput,
194
+ output: totalOutput,
195
+ cacheRead: totalCacheRead,
196
+ cacheWrite: totalCacheWrite,
197
+ durationMs: filtered[filtered.length - 1].timestamp - filtered[0].timestamp,
198
+ firstActivity: filtered[0].timestamp,
199
+ lastActivity: filtered[filtered.length - 1].timestamp,
200
+ messageCounts: {
201
+ total: filtered.length,
202
+ user: userMessages,
203
+ assistant: assistantMessages,
204
+ toolCalls: 0,
205
+ toolResults: 0,
206
+ errors: 0,
207
+ },
208
+ };
209
+ }
210
+
211
+ function renderSessionDetailPanel(
212
+ session: UsageSessionEntry,
213
+ timeSeries: { points: TimeSeriesPoint[] } | null,
214
+ timeSeriesLoading: boolean,
215
+ timeSeriesMode: "cumulative" | "per-turn",
216
+ onTimeSeriesModeChange: (mode: "cumulative" | "per-turn") => void,
217
+ timeSeriesBreakdownMode: "total" | "by-type",
218
+ onTimeSeriesBreakdownChange: (mode: "total" | "by-type") => void,
219
+ timeSeriesCursorStart: number | null,
220
+ timeSeriesCursorEnd: number | null,
221
+ onTimeSeriesCursorRangeChange: (start: number | null, end: number | null) => void,
222
+ startDate: string,
223
+ endDate: string,
224
+ selectedDays: string[],
225
+ sessionLogs: SessionLogEntry[] | null,
226
+ sessionLogsLoading: boolean,
227
+ sessionLogsExpanded: boolean,
228
+ onToggleSessionLogsExpanded: () => void,
229
+ logFilters: {
230
+ roles: SessionLogRole[];
231
+ tools: string[];
232
+ hasTools: boolean;
233
+ query: string;
234
+ },
235
+ onLogFilterRolesChange: (next: SessionLogRole[]) => void,
236
+ onLogFilterToolsChange: (next: string[]) => void,
237
+ onLogFilterHasToolsChange: (next: boolean) => void,
238
+ onLogFilterQueryChange: (next: string) => void,
239
+ onLogFilterClear: () => void,
240
+ contextExpanded: boolean,
241
+ onToggleContextExpanded: () => void,
242
+ onClose: () => void,
243
+ ) {
244
+ const label = session.label || session.key;
245
+ const displayLabel = label.length > 50 ? label.slice(0, 50) + "…" : label;
246
+ const usage = session.usage;
247
+
248
+ const hasRange = timeSeriesCursorStart !== null && timeSeriesCursorEnd !== null;
249
+ const filteredUsage =
250
+ timeSeriesCursorStart !== null && timeSeriesCursorEnd !== null && timeSeries?.points && usage
251
+ ? computeFilteredUsage(usage, timeSeries.points, timeSeriesCursorStart, timeSeriesCursorEnd)
252
+ : undefined;
253
+ const headerStats = filteredUsage
254
+ ? { totalTokens: filteredUsage.totalTokens, totalCost: filteredUsage.totalCost }
255
+ : { totalTokens: usage?.totalTokens ?? 0, totalCost: usage?.totalCost ?? 0 };
256
+ const cursorIndicator = filteredUsage ? " (filtered)" : "";
257
+
258
+ return html`
259
+ <div class="card session-detail-panel">
260
+ <div class="session-detail-header">
261
+ <div class="session-detail-header-left">
262
+ <div class="session-detail-title">
263
+ ${displayLabel}
264
+ ${cursorIndicator ? html`<span style="font-size: 11px; color: var(--muted); margin-left: 8px;">${cursorIndicator}</span>` : nothing}
265
+ </div>
266
+ </div>
267
+ <div class="session-detail-stats">
268
+ ${
269
+ usage
270
+ ? html`
271
+ <span><strong>${formatTokens(headerStats.totalTokens)}</strong> tokens${cursorIndicator}</span>
272
+ <span><strong>${formatCost(headerStats.totalCost)}</strong>${cursorIndicator}</span>
273
+ `
274
+ : nothing
275
+ }
276
+ </div>
277
+ <button class="session-close-btn" @click=${onClose} title="Close session details">×</button>
278
+ </div>
279
+ <div class="session-detail-content">
280
+ ${renderSessionSummary(
281
+ session,
282
+ filteredUsage,
283
+ timeSeriesCursorStart != null && timeSeriesCursorEnd != null && sessionLogs
284
+ ? filterLogsByRange(sessionLogs, timeSeriesCursorStart, timeSeriesCursorEnd)
285
+ : undefined,
286
+ )}
287
+ <div class="session-detail-row">
288
+ ${renderTimeSeriesCompact(
289
+ timeSeries,
290
+ timeSeriesLoading,
291
+ timeSeriesMode,
292
+ onTimeSeriesModeChange,
293
+ timeSeriesBreakdownMode,
294
+ onTimeSeriesBreakdownChange,
295
+ startDate,
296
+ endDate,
297
+ selectedDays,
298
+ timeSeriesCursorStart,
299
+ timeSeriesCursorEnd,
300
+ onTimeSeriesCursorRangeChange,
301
+ )}
302
+ </div>
303
+ <div class="session-detail-bottom">
304
+ ${renderSessionLogsCompact(
305
+ sessionLogs,
306
+ sessionLogsLoading,
307
+ sessionLogsExpanded,
308
+ onToggleSessionLogsExpanded,
309
+ logFilters,
310
+ onLogFilterRolesChange,
311
+ onLogFilterToolsChange,
312
+ onLogFilterHasToolsChange,
313
+ onLogFilterQueryChange,
314
+ onLogFilterClear,
315
+ hasRange ? timeSeriesCursorStart : null,
316
+ hasRange ? timeSeriesCursorEnd : null,
317
+ )}
318
+ ${renderContextPanel(session.contextWeight, usage, contextExpanded, onToggleContextExpanded)}
319
+ </div>
320
+ </div>
321
+ </div>
322
+ `;
323
+ }
324
+
325
+ function renderTimeSeriesCompact(
326
+ timeSeries: { points: TimeSeriesPoint[] } | null,
327
+ loading: boolean,
328
+ mode: "cumulative" | "per-turn",
329
+ onModeChange: (mode: "cumulative" | "per-turn") => void,
330
+ breakdownMode: "total" | "by-type",
331
+ onBreakdownChange: (mode: "total" | "by-type") => void,
332
+ startDate?: string,
333
+ endDate?: string,
334
+ selectedDays?: string[],
335
+ cursorStart?: number | null,
336
+ cursorEnd?: number | null,
337
+ onCursorRangeChange?: (start: number | null, end: number | null) => void,
338
+ ) {
339
+ if (loading) {
340
+ return html`
341
+ <div class="session-timeseries-compact">
342
+ <div class="muted" style="padding: 20px; text-align: center">Loading...</div>
343
+ </div>
344
+ `;
345
+ }
346
+ if (!timeSeries || timeSeries.points.length < 2) {
347
+ return html`
348
+ <div class="session-timeseries-compact">
349
+ <div class="muted" style="padding: 20px; text-align: center">No timeline data</div>
350
+ </div>
351
+ `;
352
+ }
353
+
354
+ // Filter and recalculate (same logic as main function)
355
+ let points = timeSeries.points;
356
+ if (startDate || endDate || (selectedDays && selectedDays.length > 0)) {
357
+ const startTs = startDate ? new Date(startDate + "T00:00:00").getTime() : 0;
358
+ const endTs = endDate ? new Date(endDate + "T23:59:59").getTime() : Infinity;
359
+ points = timeSeries.points.filter((p) => {
360
+ if (p.timestamp < startTs || p.timestamp > endTs) {
361
+ return false;
362
+ }
363
+ if (selectedDays && selectedDays.length > 0) {
364
+ const d = new Date(p.timestamp);
365
+ const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
366
+ return selectedDays.includes(dateStr);
367
+ }
368
+ return true;
369
+ });
370
+ }
371
+ if (points.length < 2) {
372
+ return html`
373
+ <div class="session-timeseries-compact">
374
+ <div class="muted" style="padding: 20px; text-align: center">No data in range</div>
375
+ </div>
376
+ `;
377
+ }
378
+ let cumTokens = 0,
379
+ cumCost = 0;
380
+ let sumOutput = 0;
381
+ let sumInput = 0;
382
+ let sumCacheRead = 0;
383
+ let sumCacheWrite = 0;
384
+ points = points.map((p) => {
385
+ cumTokens += p.totalTokens;
386
+ cumCost += p.cost;
387
+ sumOutput += p.output;
388
+ sumInput += p.input;
389
+ sumCacheRead += p.cacheRead;
390
+ sumCacheWrite += p.cacheWrite;
391
+ return { ...p, cumulativeTokens: cumTokens, cumulativeCost: cumCost };
392
+ });
393
+
394
+ // Compute range-filtered sums for "Tokens by Type"
395
+ const hasSelection = cursorStart != null && cursorEnd != null;
396
+ const rangeStartTs = hasSelection ? Math.min(cursorStart, cursorEnd) : 0;
397
+ const rangeEndTs = hasSelection ? Math.max(cursorStart, cursorEnd) : Infinity;
398
+
399
+ // Find start/end indices for dimming
400
+ let rangeStartIdx = 0;
401
+ let rangeEndIdx = points.length;
402
+ if (hasSelection) {
403
+ rangeStartIdx = points.findIndex((p) => p.timestamp >= rangeStartTs);
404
+ if (rangeStartIdx === -1) {
405
+ rangeStartIdx = points.length;
406
+ }
407
+ const endIdx = points.findIndex((p) => p.timestamp > rangeEndTs);
408
+ rangeEndIdx = endIdx === -1 ? points.length : endIdx;
409
+ }
410
+
411
+ const filteredPoints = hasSelection ? points.slice(rangeStartIdx, rangeEndIdx) : points;
412
+ let filteredOutput = 0,
413
+ filteredInput = 0,
414
+ filteredCacheRead = 0,
415
+ filteredCacheWrite = 0;
416
+ for (const p of filteredPoints) {
417
+ filteredOutput += p.output;
418
+ filteredInput += p.input;
419
+ filteredCacheRead += p.cacheRead;
420
+ filteredCacheWrite += p.cacheWrite;
421
+ }
422
+
423
+ const width = 400,
424
+ height = 100;
425
+ const padding = { top: 8, right: 4, bottom: 14, left: 30 };
426
+ const chartWidth = width - padding.left - padding.right;
427
+ const chartHeight = height - padding.top - padding.bottom;
428
+ const isCumulative = mode === "cumulative";
429
+ const breakdownByType = mode === "per-turn" && breakdownMode === "by-type";
430
+
431
+ const totalTypeTokens = filteredOutput + filteredInput + filteredCacheRead + filteredCacheWrite;
432
+ const barTotals = points.map((p) =>
433
+ isCumulative
434
+ ? p.cumulativeTokens
435
+ : breakdownByType
436
+ ? p.input + p.output + p.cacheRead + p.cacheWrite
437
+ : p.totalTokens,
438
+ );
439
+ const maxValue = Math.max(...barTotals, 1);
440
+ // Ensure bars + gaps fit exactly within chartWidth
441
+ const slotWidth = chartWidth / points.length; // space per bar including gap
442
+ const barWidth = Math.min(CHART_MAX_BAR_WIDTH, Math.max(1, slotWidth * CHART_BAR_WIDTH_RATIO));
443
+ const barGap = slotWidth - barWidth;
444
+
445
+ // Pre-compute handle X positions in SVG viewBox coordinates
446
+ const leftHandleX = padding.left + rangeStartIdx * (barWidth + barGap);
447
+ const rightHandleX =
448
+ rangeEndIdx >= points.length
449
+ ? padding.left + (points.length - 1) * (barWidth + barGap) + barWidth // right edge of last bar
450
+ : padding.left + (rangeEndIdx - 1) * (barWidth + barGap) + barWidth; // right edge of last selected bar
451
+
452
+ return html`
453
+ <div class="session-timeseries-compact">
454
+ <div class="timeseries-header-row">
455
+ <div class="card-title" style="font-size: 12px; color: var(--text);">Usage Over Time</div>
456
+ <div class="timeseries-controls">
457
+ ${
458
+ hasSelection
459
+ ? html`
460
+ <div class="chart-toggle small">
461
+ <button class="toggle-btn active" @click=${() => onCursorRangeChange?.(null, null)}>Reset</button>
462
+ </div>
463
+ `
464
+ : nothing
465
+ }
466
+ <div class="chart-toggle small">
467
+ <button
468
+ class="toggle-btn ${!isCumulative ? "active" : ""}"
469
+ @click=${() => onModeChange("per-turn")}
470
+ >
471
+ Per Turn
472
+ </button>
473
+ <button
474
+ class="toggle-btn ${isCumulative ? "active" : ""}"
475
+ @click=${() => onModeChange("cumulative")}
476
+ >
477
+ Cumulative
478
+ </button>
479
+ </div>
480
+ ${
481
+ !isCumulative
482
+ ? html`
483
+ <div class="chart-toggle small">
484
+ <button
485
+ class="toggle-btn ${breakdownMode === "total" ? "active" : ""}"
486
+ @click=${() => onBreakdownChange("total")}
487
+ >
488
+ Total
489
+ </button>
490
+ <button
491
+ class="toggle-btn ${breakdownMode === "by-type" ? "active" : ""}"
492
+ @click=${() => onBreakdownChange("by-type")}
493
+ >
494
+ By Type
495
+ </button>
496
+ </div>
497
+ `
498
+ : nothing
499
+ }
500
+ </div>
501
+ </div>
502
+ <div class="timeseries-chart-wrapper" style="position: relative; cursor: crosshair;">
503
+ <svg
504
+ viewBox="0 0 ${width} ${height + 18}"
505
+ class="timeseries-svg"
506
+ style="width: 100%; height: auto; display: block;"
507
+ >
508
+ <!-- Y axis -->
509
+ <line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${padding.top + chartHeight}" stroke="var(--border)" />
510
+ <!-- X axis -->
511
+ <line x1="${padding.left}" y1="${padding.top + chartHeight}" x2="${width - padding.right}" y2="${padding.top + chartHeight}" stroke="var(--border)" />
512
+ <!-- Y axis labels -->
513
+ <text x="${padding.left - 4}" y="${padding.top + 5}" text-anchor="end" class="ts-axis-label">${formatTokens(maxValue)}</text>
514
+ <text x="${padding.left - 4}" y="${padding.top + chartHeight}" text-anchor="end" class="ts-axis-label">0</text>
515
+ <!-- X axis labels (first and last) -->
516
+ ${
517
+ points.length > 0
518
+ ? svg`
519
+ <text x="${padding.left}" y="${padding.top + chartHeight + 10}" text-anchor="start" class="ts-axis-label">${new Date(points[0].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
520
+ <text x="${width - padding.right}" y="${padding.top + chartHeight + 10}" text-anchor="end" class="ts-axis-label">${new Date(points[points.length - 1].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
521
+ `
522
+ : nothing
523
+ }
524
+ <!-- Bars -->
525
+ ${points.map((p, i) => {
526
+ const val = barTotals[i];
527
+ const x = padding.left + i * (barWidth + barGap);
528
+ const bh = (val / maxValue) * chartHeight;
529
+ const y = padding.top + chartHeight - bh;
530
+ const date = new Date(p.timestamp);
531
+ const tooltipLines = [
532
+ date.toLocaleDateString(undefined, {
533
+ month: "short",
534
+ day: "numeric",
535
+ hour: "2-digit",
536
+ minute: "2-digit",
537
+ }),
538
+ `${formatTokens(val)} tokens`,
539
+ ];
540
+ if (breakdownByType) {
541
+ tooltipLines.push(`Out ${formatTokens(p.output)}`);
542
+ tooltipLines.push(`In ${formatTokens(p.input)}`);
543
+ tooltipLines.push(`CW ${formatTokens(p.cacheWrite)}`);
544
+ tooltipLines.push(`CR ${formatTokens(p.cacheRead)}`);
545
+ }
546
+ const tooltip = tooltipLines.join(" · ");
547
+ const isOutside = hasSelection && (i < rangeStartIdx || i >= rangeEndIdx);
548
+
549
+ if (!breakdownByType) {
550
+ return svg`<rect x="${x}" y="${y}" width="${barWidth}" height="${bh}" class="ts-bar${isOutside ? " dimmed" : ""}" rx="1"><title>${tooltip}</title></rect>`;
551
+ }
552
+ const segments = [
553
+ { value: p.output, cls: "output" },
554
+ { value: p.input, cls: "input" },
555
+ { value: p.cacheWrite, cls: "cache-write" },
556
+ { value: p.cacheRead, cls: "cache-read" },
557
+ ];
558
+ let yC = padding.top + chartHeight;
559
+ const dim = isOutside ? " dimmed" : "";
560
+ return svg`
561
+ ${segments.map((seg) => {
562
+ if (seg.value <= 0 || val <= 0) {
563
+ return nothing;
564
+ }
565
+ const sh = bh * (seg.value / val);
566
+ yC -= sh;
567
+ return svg`<rect x="${x}" y="${yC}" width="${barWidth}" height="${sh}" class="ts-bar ${seg.cls}${dim}" rx="1"><title>${tooltip}</title></rect>`;
568
+ })}
569
+ `;
570
+ })}
571
+ <!-- Selection highlight overlay (always visible between handles) -->
572
+ ${svg`
573
+ <rect
574
+ x="${leftHandleX}"
575
+ y="${padding.top}"
576
+ width="${Math.max(1, rightHandleX - leftHandleX)}"
577
+ height="${chartHeight}"
578
+ fill="var(--accent)"
579
+ opacity="${CHART_SELECTION_OPACITY}"
580
+ pointer-events="none"
581
+ />
582
+ `}
583
+ <!-- Left cursor line + handle -->
584
+ ${svg`
585
+ <line x1="${leftHandleX}" y1="${padding.top}" x2="${leftHandleX}" y2="${padding.top + chartHeight}" stroke="var(--accent)" stroke-width="0.8" opacity="0.7" />
586
+ <rect x="${leftHandleX - HANDLE_WIDTH / 2}" y="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 2}" width="${HANDLE_WIDTH}" height="${HANDLE_HEIGHT}" rx="1.5" fill="var(--accent)" class="cursor-handle" />
587
+ <line x1="${leftHandleX - HANDLE_GRIP_OFFSET}" y1="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 5}" x2="${leftHandleX - HANDLE_GRIP_OFFSET}" y2="${padding.top + chartHeight / 2 + HANDLE_HEIGHT / 5}" stroke="var(--bg)" stroke-width="0.4" pointer-events="none" />
588
+ <line x1="${leftHandleX + HANDLE_GRIP_OFFSET}" y1="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 5}" x2="${leftHandleX + HANDLE_GRIP_OFFSET}" y2="${padding.top + chartHeight / 2 + HANDLE_HEIGHT / 5}" stroke="var(--bg)" stroke-width="0.4" pointer-events="none" />
589
+ `}
590
+ <!-- Right cursor line + handle -->
591
+ ${svg`
592
+ <line x1="${rightHandleX}" y1="${padding.top}" x2="${rightHandleX}" y2="${padding.top + chartHeight}" stroke="var(--accent)" stroke-width="0.8" opacity="0.7" />
593
+ <rect x="${rightHandleX - HANDLE_WIDTH / 2}" y="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 2}" width="${HANDLE_WIDTH}" height="${HANDLE_HEIGHT}" rx="1.5" fill="var(--accent)" class="cursor-handle" />
594
+ <line x1="${rightHandleX - HANDLE_GRIP_OFFSET}" y1="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 5}" x2="${rightHandleX - HANDLE_GRIP_OFFSET}" y2="${padding.top + chartHeight / 2 + HANDLE_HEIGHT / 5}" stroke="var(--bg)" stroke-width="0.4" pointer-events="none" />
595
+ <line x1="${rightHandleX + HANDLE_GRIP_OFFSET}" y1="${padding.top + chartHeight / 2 - HANDLE_HEIGHT / 5}" x2="${rightHandleX + HANDLE_GRIP_OFFSET}" y2="${padding.top + chartHeight / 2 + HANDLE_HEIGHT / 5}" stroke="var(--bg)" stroke-width="0.4" pointer-events="none" />
596
+ `}
597
+ </svg>
598
+ <!-- Handle drag zones (only on handles, not full chart) -->
599
+ ${(() => {
600
+ const leftHandlePos = `${((leftHandleX / width) * 100).toFixed(1)}%`;
601
+ const rightHandlePos = `${((rightHandleX / width) * 100).toFixed(1)}%`;
602
+
603
+ const makeDragHandler = (side: "left" | "right") => (e: MouseEvent) => {
604
+ if (!onCursorRangeChange) {
605
+ return;
606
+ }
607
+ e.preventDefault();
608
+ e.stopPropagation();
609
+ // Find the wrapper, then the SVG inside it
610
+ const wrapper = (e.currentTarget as HTMLElement).closest(".timeseries-chart-wrapper");
611
+ const svgEl = wrapper?.querySelector("svg") as SVGSVGElement;
612
+ if (!svgEl) {
613
+ return;
614
+ }
615
+ // Capture rect once at mousedown to avoid re-render offset shifts
616
+ const rect = svgEl.getBoundingClientRect();
617
+ const svgWidth = rect.width;
618
+ const chartLeftPx = (padding.left / width) * svgWidth;
619
+ const chartRightPx = ((width - padding.right) / width) * svgWidth;
620
+ const chartW = chartRightPx - chartLeftPx;
621
+
622
+ const posToIdx = (clientX: number) => {
623
+ const x = Math.max(0, Math.min(1, (clientX - rect.left - chartLeftPx) / chartW));
624
+ return Math.min(Math.floor(x * points.length), points.length - 1);
625
+ };
626
+
627
+ // Compute click offset: where on the handle the user grabbed
628
+ const handleSvgX = side === "left" ? leftHandleX : rightHandleX;
629
+ const handleClientX = rect.left + (handleSvgX / width) * svgWidth;
630
+ const grabOffset = e.clientX - handleClientX;
631
+
632
+ document.body.style.cursor = "col-resize";
633
+
634
+ const handleMove = (me: MouseEvent) => {
635
+ const adjustedX = me.clientX - grabOffset;
636
+ const idx = posToIdx(adjustedX);
637
+ const pt = points[idx];
638
+ if (!pt) {
639
+ return;
640
+ }
641
+ if (side === "left") {
642
+ const endTs = cursorEnd ?? points[points.length - 1].timestamp;
643
+ // Don't let left go past right
644
+ onCursorRangeChange(Math.min(pt.timestamp, endTs), endTs);
645
+ } else {
646
+ const startTs = cursorStart ?? points[0].timestamp;
647
+ // Don't let right go past left
648
+ onCursorRangeChange(startTs, Math.max(pt.timestamp, startTs));
649
+ }
650
+ };
651
+
652
+ const handleUp = () => {
653
+ document.body.style.cursor = "";
654
+ document.removeEventListener("mousemove", handleMove);
655
+ document.removeEventListener("mouseup", handleUp);
656
+ };
657
+
658
+ document.addEventListener("mousemove", handleMove);
659
+ document.addEventListener("mouseup", handleUp);
660
+ };
661
+
662
+ return html`
663
+ <div class="chart-handle-zone chart-handle-left"
664
+ style="left: ${leftHandlePos};"
665
+ @mousedown=${makeDragHandler("left")}></div>
666
+ <div class="chart-handle-zone chart-handle-right"
667
+ style="left: ${rightHandlePos};"
668
+ @mousedown=${makeDragHandler("right")}></div>
669
+ `;
670
+ })()}
671
+ </div>
672
+ <div class="timeseries-summary">
673
+ ${
674
+ hasSelection
675
+ ? html`
676
+ <span style="color: var(--accent);">▶ Turns ${rangeStartIdx + 1}–${rangeEndIdx} of ${points.length}</span> ·
677
+ ${new Date(rangeStartTs).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}–${new Date(rangeEndTs).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} ·
678
+ ${formatTokens(filteredOutput + filteredInput + filteredCacheRead + filteredCacheWrite)} ·
679
+ ${formatCost(filteredPoints.reduce((s, p) => s + (p.cost || 0), 0))}
680
+ `
681
+ : html`${points.length} msgs · ${formatTokens(cumTokens)} · ${formatCost(cumCost)}`
682
+ }
683
+ </div>
684
+ ${
685
+ breakdownByType
686
+ ? html`
687
+ <div style="margin-top: 8px;">
688
+ <div class="card-title" style="font-size: 12px; margin-bottom: 6px; color: var(--text);">Tokens by Type</div>
689
+ <div class="cost-breakdown-bar" style="height: 18px;">
690
+ <div class="cost-segment output" style="width: ${pct(filteredOutput, totalTypeTokens).toFixed(1)}%"></div>
691
+ <div class="cost-segment input" style="width: ${pct(filteredInput, totalTypeTokens).toFixed(1)}%"></div>
692
+ <div class="cost-segment cache-write" style="width: ${pct(filteredCacheWrite, totalTypeTokens).toFixed(1)}%"></div>
693
+ <div class="cost-segment cache-read" style="width: ${pct(filteredCacheRead, totalTypeTokens).toFixed(1)}%"></div>
694
+ </div>
695
+ <div class="cost-breakdown-legend">
696
+ <div class="legend-item" title="Assistant output tokens">
697
+ <span class="legend-dot output"></span>Output ${formatTokens(filteredOutput)}
698
+ </div>
699
+ <div class="legend-item" title="User + tool input tokens">
700
+ <span class="legend-dot input"></span>Input ${formatTokens(filteredInput)}
701
+ </div>
702
+ <div class="legend-item" title="Tokens written to cache">
703
+ <span class="legend-dot cache-write"></span>Cache Write ${formatTokens(filteredCacheWrite)}
704
+ </div>
705
+ <div class="legend-item" title="Tokens read from cache">
706
+ <span class="legend-dot cache-read"></span>Cache Read ${formatTokens(filteredCacheRead)}
707
+ </div>
708
+ </div>
709
+ <div class="cost-breakdown-total">Total: ${formatTokens(totalTypeTokens)}</div>
710
+ </div>
711
+ `
712
+ : nothing
713
+ }
714
+ </div>
715
+ `;
716
+ }
717
+
718
+ function renderContextPanel(
719
+ contextWeight: UsageSessionEntry["contextWeight"],
720
+ usage: UsageSessionEntry["usage"],
721
+ expanded: boolean,
722
+ onToggleExpanded: () => void,
723
+ ) {
724
+ if (!contextWeight) {
725
+ return html`
726
+ <div class="context-details-panel">
727
+ <div class="muted" style="padding: 20px; text-align: center">No context data</div>
728
+ </div>
729
+ `;
730
+ }
731
+ const systemTokens = charsToTokens(contextWeight.systemPrompt.chars);
732
+ const skillsTokens = charsToTokens(contextWeight.skills.promptChars);
733
+ const toolsTokens = charsToTokens(
734
+ contextWeight.tools.listChars + contextWeight.tools.schemaChars,
735
+ );
736
+ const filesTokens = charsToTokens(
737
+ contextWeight.injectedWorkspaceFiles.reduce((sum, f) => sum + f.injectedChars, 0),
738
+ );
739
+ const totalContextTokens = systemTokens + skillsTokens + toolsTokens + filesTokens;
740
+
741
+ let contextPct = "";
742
+ if (usage && usage.totalTokens > 0) {
743
+ const inputTokens = usage.input + usage.cacheRead;
744
+ if (inputTokens > 0) {
745
+ contextPct = `~${Math.min((totalContextTokens / inputTokens) * 100, 100).toFixed(0)}% of input`;
746
+ }
747
+ }
748
+
749
+ const skillsList = contextWeight.skills.entries.toSorted((a, b) => b.blockChars - a.blockChars);
750
+ const toolsList = contextWeight.tools.entries.toSorted(
751
+ (a, b) => b.summaryChars + b.schemaChars - (a.summaryChars + a.schemaChars),
752
+ );
753
+ const filesList = contextWeight.injectedWorkspaceFiles.toSorted(
754
+ (a, b) => b.injectedChars - a.injectedChars,
755
+ );
756
+ const defaultLimit = 4;
757
+ const showAll = expanded;
758
+ const skillsTop = showAll ? skillsList : skillsList.slice(0, defaultLimit);
759
+ const toolsTop = showAll ? toolsList : toolsList.slice(0, defaultLimit);
760
+ const filesTop = showAll ? filesList : filesList.slice(0, defaultLimit);
761
+ const hasMore =
762
+ skillsList.length > defaultLimit ||
763
+ toolsList.length > defaultLimit ||
764
+ filesList.length > defaultLimit;
765
+
766
+ return html`
767
+ <div class="context-details-panel">
768
+ <div class="context-breakdown-header">
769
+ <div class="card-title" style="font-size: 12px; color: var(--text);">System Prompt Breakdown</div>
770
+ ${
771
+ hasMore
772
+ ? html`<button class="context-expand-btn" @click=${onToggleExpanded}>
773
+ ${showAll ? "Collapse" : "Expand all"}
774
+ </button>`
775
+ : nothing
776
+ }
777
+ </div>
778
+ <p class="context-weight-desc">
779
+ ${contextPct || "Base context per message"}
780
+ </p>
781
+ <div class="context-stacked-bar">
782
+ <div class="context-segment system" style="width: ${pct(systemTokens, totalContextTokens).toFixed(1)}%" title="System: ~${formatTokens(systemTokens)}"></div>
783
+ <div class="context-segment skills" style="width: ${pct(skillsTokens, totalContextTokens).toFixed(1)}%" title="Skills: ~${formatTokens(skillsTokens)}"></div>
784
+ <div class="context-segment tools" style="width: ${pct(toolsTokens, totalContextTokens).toFixed(1)}%" title="Tools: ~${formatTokens(toolsTokens)}"></div>
785
+ <div class="context-segment files" style="width: ${pct(filesTokens, totalContextTokens).toFixed(1)}%" title="Files: ~${formatTokens(filesTokens)}"></div>
786
+ </div>
787
+ <div class="context-legend">
788
+ <span class="legend-item"><span class="legend-dot system"></span>Sys ~${formatTokens(systemTokens)}</span>
789
+ <span class="legend-item"><span class="legend-dot skills"></span>Skills ~${formatTokens(skillsTokens)}</span>
790
+ <span class="legend-item"><span class="legend-dot tools"></span>Tools ~${formatTokens(toolsTokens)}</span>
791
+ <span class="legend-item"><span class="legend-dot files"></span>Files ~${formatTokens(filesTokens)}</span>
792
+ </div>
793
+ <div class="context-total">Total: ~${formatTokens(totalContextTokens)}</div>
794
+ <div class="context-breakdown-grid">
795
+ ${
796
+ skillsList.length > 0
797
+ ? (() => {
798
+ const more = skillsList.length - skillsTop.length;
799
+ return html`
800
+ <div class="context-breakdown-card">
801
+ <div class="context-breakdown-title">Skills (${skillsList.length})</div>
802
+ <div class="context-breakdown-list">
803
+ ${skillsTop.map(
804
+ (s) => html`
805
+ <div class="context-breakdown-item">
806
+ <span class="mono">${s.name}</span>
807
+ <span class="muted">~${formatTokens(charsToTokens(s.blockChars))}</span>
808
+ </div>
809
+ `,
810
+ )}
811
+ </div>
812
+ ${
813
+ more > 0
814
+ ? html`<div class="context-breakdown-more">+${more} more</div>`
815
+ : nothing
816
+ }
817
+ </div>
818
+ `;
819
+ })()
820
+ : nothing
821
+ }
822
+ ${
823
+ toolsList.length > 0
824
+ ? (() => {
825
+ const more = toolsList.length - toolsTop.length;
826
+ return html`
827
+ <div class="context-breakdown-card">
828
+ <div class="context-breakdown-title">Tools (${toolsList.length})</div>
829
+ <div class="context-breakdown-list">
830
+ ${toolsTop.map(
831
+ (t) => html`
832
+ <div class="context-breakdown-item">
833
+ <span class="mono">${t.name}</span>
834
+ <span class="muted">~${formatTokens(charsToTokens(t.summaryChars + t.schemaChars))}</span>
835
+ </div>
836
+ `,
837
+ )}
838
+ </div>
839
+ ${
840
+ more > 0
841
+ ? html`<div class="context-breakdown-more">+${more} more</div>`
842
+ : nothing
843
+ }
844
+ </div>
845
+ `;
846
+ })()
847
+ : nothing
848
+ }
849
+ ${
850
+ filesList.length > 0
851
+ ? (() => {
852
+ const more = filesList.length - filesTop.length;
853
+ return html`
854
+ <div class="context-breakdown-card">
855
+ <div class="context-breakdown-title">Files (${filesList.length})</div>
856
+ <div class="context-breakdown-list">
857
+ ${filesTop.map(
858
+ (f) => html`
859
+ <div class="context-breakdown-item">
860
+ <span class="mono">${f.name}</span>
861
+ <span class="muted">~${formatTokens(charsToTokens(f.injectedChars))}</span>
862
+ </div>
863
+ `,
864
+ )}
865
+ </div>
866
+ ${
867
+ more > 0
868
+ ? html`<div class="context-breakdown-more">+${more} more</div>`
869
+ : nothing
870
+ }
871
+ </div>
872
+ `;
873
+ })()
874
+ : nothing
875
+ }
876
+ </div>
877
+ </div>
878
+ `;
879
+ }
880
+
881
+ function renderSessionLogsCompact(
882
+ logs: SessionLogEntry[] | null,
883
+ loading: boolean,
884
+ expandedAll: boolean,
885
+ onToggleExpandedAll: () => void,
886
+ filters: {
887
+ roles: SessionLogRole[];
888
+ tools: string[];
889
+ hasTools: boolean;
890
+ query: string;
891
+ },
892
+ onFilterRolesChange: (next: SessionLogRole[]) => void,
893
+ onFilterToolsChange: (next: string[]) => void,
894
+ onFilterHasToolsChange: (next: boolean) => void,
895
+ onFilterQueryChange: (next: string) => void,
896
+ onFilterClear: () => void,
897
+ cursorStart?: number | null,
898
+ cursorEnd?: number | null,
899
+ ) {
900
+ if (loading) {
901
+ return html`
902
+ <div class="session-logs-compact">
903
+ <div class="session-logs-header">Conversation</div>
904
+ <div class="muted" style="padding: 20px; text-align: center">Loading...</div>
905
+ </div>
906
+ `;
907
+ }
908
+ if (!logs || logs.length === 0) {
909
+ return html`
910
+ <div class="session-logs-compact">
911
+ <div class="session-logs-header">Conversation</div>
912
+ <div class="muted" style="padding: 20px; text-align: center">No messages</div>
913
+ </div>
914
+ `;
915
+ }
916
+
917
+ const normalizedQuery = filters.query.trim().toLowerCase();
918
+ const entries = logs.map((log) => {
919
+ const toolInfo = parseToolSummary(log.content);
920
+ const cleanContent = toolInfo.cleanContent || log.content;
921
+ return { log, toolInfo, cleanContent };
922
+ });
923
+ const toolOptions = Array.from(
924
+ new Set(entries.flatMap((entry) => entry.toolInfo.tools.map(([name]) => name))),
925
+ ).toSorted((a, b) => a.localeCompare(b));
926
+ const filteredEntries = entries.filter((entry) => {
927
+ // Filter by cursor timeline range (only if logs cover the range)
928
+ if (cursorStart != null && cursorEnd != null) {
929
+ const ts = entry.log.timestamp;
930
+ if (ts > 0) {
931
+ const lo = Math.min(cursorStart, cursorEnd);
932
+ const hi = Math.max(cursorStart, cursorEnd);
933
+ const normalizedTs = normalizeLogTimestamp(ts);
934
+ if (normalizedTs < lo || normalizedTs > hi) {
935
+ return false;
936
+ }
937
+ }
938
+ }
939
+ if (filters.roles.length > 0 && !filters.roles.includes(entry.log.role)) {
940
+ return false;
941
+ }
942
+ if (filters.hasTools && entry.toolInfo.tools.length === 0) {
943
+ return false;
944
+ }
945
+ if (filters.tools.length > 0) {
946
+ const matchesTool = entry.toolInfo.tools.some(([name]) => filters.tools.includes(name));
947
+ if (!matchesTool) {
948
+ return false;
949
+ }
950
+ }
951
+ if (normalizedQuery) {
952
+ const haystack = entry.cleanContent.toLowerCase();
953
+ if (!haystack.includes(normalizedQuery)) {
954
+ return false;
955
+ }
956
+ }
957
+ return true;
958
+ });
959
+ const hasActiveFilters =
960
+ filters.roles.length > 0 || filters.tools.length > 0 || filters.hasTools || normalizedQuery;
961
+ const hasCursorFilter = cursorStart != null && cursorEnd != null;
962
+ const displayedCount =
963
+ hasActiveFilters || hasCursorFilter
964
+ ? `${filteredEntries.length} of ${logs.length} ${hasCursorFilter ? "(timeline filtered)" : ""}`
965
+ : `${logs.length}`;
966
+
967
+ const roleSelected = new Set(filters.roles);
968
+ const toolSelected = new Set(filters.tools);
969
+
970
+ return html`
971
+ <div class="session-logs-compact">
972
+ <div class="session-logs-header">
973
+ <span>Conversation <span style="font-weight: normal; color: var(--muted);">(${displayedCount} messages)</span></span>
974
+ <button class="btn btn-sm usage-action-btn usage-secondary-btn" @click=${onToggleExpandedAll}>
975
+ ${expandedAll ? "Collapse All" : "Expand All"}
976
+ </button>
977
+ </div>
978
+ <div class="usage-filters-inline" style="margin: 10px 12px;">
979
+ <select
980
+ multiple
981
+ size="4"
982
+ @change=${(event: Event) =>
983
+ onFilterRolesChange(
984
+ Array.from((event.target as HTMLSelectElement).selectedOptions).map(
985
+ (option) => option.value as SessionLogRole,
986
+ ),
987
+ )}
988
+ >
989
+ <option value="user" ?selected=${roleSelected.has("user")}>User</option>
990
+ <option value="assistant" ?selected=${roleSelected.has("assistant")}>Assistant</option>
991
+ <option value="tool" ?selected=${roleSelected.has("tool")}>Tool</option>
992
+ <option value="toolResult" ?selected=${roleSelected.has("toolResult")}>Tool result</option>
993
+ </select>
994
+ <select
995
+ multiple
996
+ size="4"
997
+ @change=${(event: Event) =>
998
+ onFilterToolsChange(
999
+ Array.from((event.target as HTMLSelectElement).selectedOptions).map(
1000
+ (option) => option.value,
1001
+ ),
1002
+ )}
1003
+ >
1004
+ ${toolOptions.map(
1005
+ (tool) =>
1006
+ html`<option value=${tool} ?selected=${toolSelected.has(tool)}>${tool}</option>`,
1007
+ )}
1008
+ </select>
1009
+ <label class="usage-filters-inline" style="gap: 6px;">
1010
+ <input
1011
+ type="checkbox"
1012
+ .checked=${filters.hasTools}
1013
+ @change=${(event: Event) =>
1014
+ onFilterHasToolsChange((event.target as HTMLInputElement).checked)}
1015
+ />
1016
+ Has tools
1017
+ </label>
1018
+ <input
1019
+ type="text"
1020
+ placeholder="Search conversation"
1021
+ .value=${filters.query}
1022
+ @input=${(event: Event) => onFilterQueryChange((event.target as HTMLInputElement).value)}
1023
+ />
1024
+ <button class="btn btn-sm usage-action-btn usage-secondary-btn" @click=${onFilterClear}>
1025
+ Clear
1026
+ </button>
1027
+ </div>
1028
+ <div class="session-logs-list">
1029
+ ${filteredEntries.map((entry) => {
1030
+ const { log, toolInfo, cleanContent } = entry;
1031
+ const roleClass = log.role === "user" ? "user" : "assistant";
1032
+ const roleLabel =
1033
+ log.role === "user" ? "You" : log.role === "assistant" ? "Assistant" : "Tool";
1034
+ return html`
1035
+ <div class="session-log-entry ${roleClass}">
1036
+ <div class="session-log-meta">
1037
+ <span class="session-log-role">${roleLabel}</span>
1038
+ <span>${new Date(log.timestamp).toLocaleString()}</span>
1039
+ ${log.tokens ? html`<span>${formatTokens(log.tokens)}</span>` : nothing}
1040
+ </div>
1041
+ <div class="session-log-content">${cleanContent}</div>
1042
+ ${
1043
+ toolInfo.tools.length > 0
1044
+ ? html`
1045
+ <details class="session-log-tools" ?open=${expandedAll}>
1046
+ <summary>${toolInfo.summary}</summary>
1047
+ <div class="session-log-tools-list">
1048
+ ${toolInfo.tools.map(
1049
+ ([name, count]) => html`
1050
+ <span class="session-log-tools-pill">${name} × ${count}</span>
1051
+ `,
1052
+ )}
1053
+ </div>
1054
+ </details>
1055
+ `
1056
+ : nothing
1057
+ }
1058
+ </div>
1059
+ `;
1060
+ })}
1061
+ ${
1062
+ filteredEntries.length === 0
1063
+ ? html`
1064
+ <div class="muted" style="padding: 12px">No messages match the filters.</div>
1065
+ `
1066
+ : nothing
1067
+ }
1068
+ </div>
1069
+ </div>
1070
+ `;
1071
+ }
1072
+
1073
+ export {
1074
+ computeFilteredUsage,
1075
+ renderContextPanel,
1076
+ renderEmptyDetailState,
1077
+ renderSessionDetailPanel,
1078
+ renderSessionLogsCompact,
1079
+ renderSessionSummary,
1080
+ renderTimeSeriesCompact,
1081
+ CHART_BAR_WIDTH_RATIO,
1082
+ CHART_MAX_BAR_WIDTH,
1083
+ };