@nextclaw/ui 0.17.1 → 0.18.0

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 (236) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/{abnfDiagram-VRR7QNED-BcFJp_1X.js → abnfDiagram-VRR7QNED-DvTjgVMf.js} +1 -1
  3. package/dist/assets/{aiden0z-pptx-renderer.es-Dp2ezlQn.js → aiden0z-pptx-renderer.es-C1nfBAXR.js} +1 -1
  4. package/dist/assets/{api-DJ6ZKWFY.js → api-BJioGtWS.js} +3 -3
  5. package/dist/assets/{appearance-settings-page-B-juoJ6F.js → appearance-settings-page-DAF1fd14.js} +1 -1
  6. package/dist/assets/{arc-CkgyPMyW.js → arc-BvhODBXv.js} +1 -1
  7. package/dist/assets/architecture-TIHT7OUA-Bjp9jf0h.js +1 -0
  8. package/dist/assets/{architectureDiagram-ZJ3FMSHR-CMd4cIjI.js → architectureDiagram-ZJ3FMSHR-BKO9RgNS.js} +1 -1
  9. package/dist/assets/{blockDiagram-677ZJIJ3-Bz3tbhet.js → blockDiagram-677ZJIJ3-DHpDXy7D.js} +1 -1
  10. package/dist/assets/book-open-aDv6R1av.js +1 -0
  11. package/dist/assets/{c4Diagram-LMCZKHZV-D9hFNAGv.js → c4Diagram-LMCZKHZV-BG_iQnDr.js} +1 -1
  12. package/dist/assets/channel-5T3pdR-s.js +1 -0
  13. package/dist/assets/{channels-list-page-mVHYa7xW.js → channels-list-page-Dubt8r9S.js} +2 -2
  14. package/dist/assets/{chat-page-DsMYGNee.js → chat-page-JbwjeUo4.js} +1 -1
  15. package/dist/assets/chunk-2Q5K7J3B-DTZ3tGfE.js +1 -0
  16. package/dist/assets/{chunk-32BRIVSS-7NrjEITq.js → chunk-32BRIVSS-DR9M0LLx.js} +1 -1
  17. package/dist/assets/{chunk-52WLFC77-viIcqj9q.js → chunk-52WLFC77-698iC3BB.js} +1 -1
  18. package/dist/assets/{chunk-5FCAYU7R-Vsu4J-VM.js → chunk-5FCAYU7R-DikTj9Ey.js} +1 -1
  19. package/dist/assets/{chunk-5HE753X5-C-w6CQZK.js → chunk-5HE753X5-ko0o7_q0.js} +1 -1
  20. package/dist/assets/{chunk-5JV3BV7I-CuKgJQ4W.js → chunk-5JV3BV7I-CEHufhgo.js} +1 -1
  21. package/dist/assets/{chunk-5TONJI2A-CYeiWZKb.js → chunk-5TONJI2A-CGgno1cu.js} +1 -1
  22. package/dist/assets/{chunk-5VM5RSS4-DRCbfwIn.js → chunk-5VM5RSS4-DkiB4cUQ.js} +1 -1
  23. package/dist/assets/{chunk-7BUUIJ7U-BvydOcm3.js → chunk-7BUUIJ7U-DzptcytQ.js} +1 -1
  24. package/dist/assets/{chunk-BIQX33UG-RlppOXKo.js → chunk-BIQX33UG-DMRSQJvT.js} +1 -1
  25. package/dist/assets/{chunk-C7G6YPKG-DAizzYwr.js → chunk-C7G6YPKG-Bw1BwwuY.js} +1 -1
  26. package/dist/assets/{chunk-CQNSW5MT-5eChJHca.js → chunk-CQNSW5MT-C0z6eHBk.js} +1 -1
  27. package/dist/assets/{chunk-CYSBUYHQ-YHPr53fv.js → chunk-CYSBUYHQ-ByPyAouO.js} +1 -1
  28. package/dist/assets/{chunk-EMLP6XTP-CK4D7Hz5.js → chunk-EMLP6XTP-CJlAXWZg.js} +1 -1
  29. package/dist/assets/{chunk-EX3LRPZG-BmlDFIRW.js → chunk-EX3LRPZG-Dqq4zKJu.js} +1 -1
  30. package/dist/assets/{chunk-FWX5IMBZ-D0ksHZZ9.js → chunk-FWX5IMBZ-B0SNoCmi.js} +2 -2
  31. package/dist/assets/{chunk-HOUHSVGY-DPkq8FCm.js → chunk-HOUHSVGY-DG_GJih_.js} +1 -1
  32. package/dist/assets/{chunk-ICXQ74PX-FAatv5fC.js → chunk-ICXQ74PX-Drxk_6bE.js} +1 -1
  33. package/dist/assets/{chunk-JG7HCLWE-DLkIi4F7.js → chunk-JG7HCLWE-DdLFPpnd.js} +1 -1
  34. package/dist/assets/{chunk-JWPE2WC7-DawOjNE3.js → chunk-JWPE2WC7-B8M4G76F.js} +1 -1
  35. package/dist/assets/{chunk-MOJQB5TN-Di52TorW.js → chunk-MOJQB5TN-XP6FJHY_.js} +1 -1
  36. package/dist/assets/{chunk-MOZMSUNE-PfyK9w1-.js → chunk-MOZMSUNE-C_6x18Qw.js} +1 -1
  37. package/dist/assets/{chunk-OGEWGWER-C3jdH3Ty.js → chunk-OGEWGWER-N25BzCmG.js} +1 -1
  38. package/dist/assets/{chunk-OSBZ3O6U-CecHo-f0.js → chunk-OSBZ3O6U-ASDEm7y0.js} +1 -1
  39. package/dist/assets/{chunk-PUDLZKDR-f3IJh3sN.js → chunk-PUDLZKDR-CTlvAcn7.js} +1 -1
  40. package/dist/assets/{chunk-Q4XR5HBZ-DQFeUPxZ.js → chunk-Q4XR5HBZ-Da9BWfB6.js} +1 -1
  41. package/dist/assets/{chunk-QBLGF6JB-BhUeG0y2.js → chunk-QBLGF6JB-Bi6RpTJO.js} +1 -1
  42. package/dist/assets/{chunk-R7FJI6CG-C3XpZ-2u.js → chunk-R7FJI6CG-DkMsSPvK.js} +1 -1
  43. package/dist/assets/{chunk-RYQCIY6F-CAqa5klQ.js → chunk-RYQCIY6F-DZp0eU-j.js} +1 -1
  44. package/dist/assets/{chunk-U6XO7XAA-D_WXezeD.js → chunk-U6XO7XAA-vfinDSrj.js} +1 -1
  45. package/dist/assets/{chunk-V7JOEXUC-CZ70oOtp.js → chunk-V7JOEXUC-C9Nksccs.js} +1 -1
  46. package/dist/assets/chunk-VAUOI2AC-HLGIHQ6k.js +1 -0
  47. package/dist/assets/{chunk-VR4S4FIN-B7AHEYYk.js → chunk-VR4S4FIN-Xv6zfwXs.js} +1 -1
  48. package/dist/assets/{chunk-WYO6CB5R-Dn9To_OI.js → chunk-WYO6CB5R-mOJNzbWl.js} +2 -2
  49. package/dist/assets/chunk-XXDRQBXY-Dl0m6Ni-.js +1 -0
  50. package/dist/assets/{chunk-YOTPTUD7-BYEgRBk3.js → chunk-YOTPTUD7-1-aKSu4o.js} +1 -1
  51. package/dist/assets/{chunk-ZGVPDNZ5-C6YtZDU0.js → chunk-ZGVPDNZ5-OJ8veMpK.js} +1 -1
  52. package/dist/assets/{chunk-ZIRB5QZD-DMuYtnHF.js → chunk-ZIRB5QZD-SR_Nq_PP.js} +1 -1
  53. package/dist/assets/classDiagram-OUVF2IWQ-DcDnzGNX.js +1 -0
  54. package/dist/assets/classDiagram-v2-EOCWNBFH-BKXKMzKM.js +1 -0
  55. package/dist/assets/{config-split-page-BaqLcRxk.js → config-split-page-Bs1wArsX.js} +1 -1
  56. package/dist/assets/{confirm-dialog-DasFoaCT.js → confirm-dialog-BFatCZUq.js} +1 -1
  57. package/dist/assets/{cose-bilkent-JH36ORCC-Bc2Ex5Y3.js → cose-bilkent-JH36ORCC-NjVgz2Bw.js} +1 -1
  58. package/dist/assets/cynefin-VYW2F7L2-BWg7U2UD.js +1 -0
  59. package/dist/assets/{cynefinDiagram-TSTJHNR4-DVysQoGz.js → cynefinDiagram-TSTJHNR4-B_17LDqD.js} +1 -1
  60. package/dist/assets/{dagre-CMhQ1cAG.js → dagre-CMe101gk.js} +1 -1
  61. package/dist/assets/{dagre-VKFMJZFB-RYyWpPJZ.js → dagre-VKFMJZFB-qo8nLb3B.js} +1 -1
  62. package/dist/assets/{desktop-update-config-CoEvKrpB.js → desktop-update-config-DFefhK96.js} +1 -1
  63. package/dist/assets/{diagram-FQU43EPY-BD6KONdF.js → diagram-FQU43EPY-Ba3XIklC.js} +1 -1
  64. package/dist/assets/{diagram-G47NLZAW-CGB2ptRR.js → diagram-G47NLZAW-CG45TcUx.js} +1 -1
  65. package/dist/assets/{diagram-NH7WQ7WH-DnkOI6cU.js → diagram-NH7WQ7WH-2F2HrwV-.js} +1 -1
  66. package/dist/assets/{diagram-OA4YK3LP-DF82RZ7P.js → diagram-OA4YK3LP-CXfWH9NZ.js} +1 -1
  67. package/dist/assets/{diagram-WEI45ONY-Be2v7Ck9.js → diagram-WEI45ONY-CH_4yepc.js} +1 -1
  68. package/dist/assets/{dist-C741xUwt.js → dist-CV9wIyME.js} +1 -1
  69. package/dist/assets/{dist-zOKNc7QQ.js → dist-D1I3blO_.js} +1 -1
  70. package/dist/assets/doc-browser-BFfstBoh.js +1 -0
  71. package/dist/assets/doc-browser-DEdoMb3X.js +1 -0
  72. package/dist/assets/{doc-browser-context-BT8NWQyM.js → doc-browser-context-BcwcVwX_.js} +1 -1
  73. package/dist/assets/{docx-preview-BqwAjrj3.js → docx-preview-CoM4JK7_.js} +1 -1
  74. package/dist/assets/{ebnfDiagram-CCIWWBDH-CP98OYk8.js → ebnfDiagram-CCIWWBDH-BwiG4Ea1.js} +1 -1
  75. package/dist/assets/{erDiagram-Q63AITRT-ZLlx8xfs.js → erDiagram-Q63AITRT-BML3hpnQ.js} +1 -1
  76. package/dist/assets/eventmodeling-45OFAUF4-SB-J4tpq.js +1 -0
  77. package/dist/assets/flowDiagram-23GEKE2U-BQ_u-eLM.js +1 -0
  78. package/dist/assets/{form-actions-BcukaMyR.js → form-actions-YSnn7q_U.js} +1 -1
  79. package/dist/assets/{ganttDiagram-NO4QXBWP-DXCDjHR2.js → ganttDiagram-NO4QXBWP-80enfWbD.js} +1 -1
  80. package/dist/assets/gitGraph-TEB2WS4Q-DPxNwsFd.js +1 -0
  81. package/dist/assets/{gitGraphDiagram-IHSO6WYX-l2HWrTJp.js → gitGraphDiagram-IHSO6WYX-CVLqXr-b.js} +1 -1
  82. package/dist/assets/index-Bsoscjk7.css +1 -0
  83. package/dist/assets/index-L58BO782.js +109 -0
  84. package/dist/assets/info-DKCQHKI2-DNlNGH90.js +1 -0
  85. package/dist/assets/{infoDiagram-FWYZ7A6U-D-DjX-bn.js → infoDiagram-FWYZ7A6U-Dyvs2t0i.js} +1 -1
  86. package/dist/assets/{ishikawaDiagram-FXEZZL3T-y_Xy128F.js → ishikawaDiagram-FXEZZL3T-sSnxDKfh.js} +1 -1
  87. package/dist/assets/{journeyDiagram-5HDEW3XC-DJn5xYcQ.js → journeyDiagram-5HDEW3XC-D22V71Mf.js} +1 -1
  88. package/dist/assets/{kanban-definition-HUTT4EX6-DJVIOR21.js → kanban-definition-HUTT4EX6-D3ds0p-g.js} +1 -1
  89. package/dist/assets/{line-Cos7PIOj.js → line-CbyFs065.js} +1 -1
  90. package/dist/assets/{linear-BtDLqtzF.js → linear-4DKtnZBS.js} +1 -1
  91. package/dist/assets/loader-circle-CsNiT6VM.js +1 -0
  92. package/dist/assets/{map-BKnQyvd0.js → map-CcZzEoz6.js} +1 -1
  93. package/dist/assets/mcp-marketplace-page-DfUCF-L5.js +1 -0
  94. package/dist/assets/mcp-marketplace-page-ZTHpQTvi.js +9 -0
  95. package/dist/assets/{mermaid-parser.core-XUNZhr2W.js → mermaid-parser.core-DPmMOjXD.js} +2 -2
  96. package/dist/assets/{mermaid.core-ySrvolIe.js → mermaid.core-BSf-ddRa.js} +3 -3
  97. package/dist/assets/{mindmap-definition-LN4V7U3C-BypL9tei.js → mindmap-definition-LN4V7U3C-BUTwWPyx.js} +1 -1
  98. package/dist/assets/model-config-page-jYCwpq4U.js +1 -0
  99. package/dist/assets/{navigation-link-BoMNAKyw.js → navigation-link-7oSjAC9R.js} +1 -1
  100. package/dist/assets/{ordinal-CxxVWxYU.js → ordinal-BgLYtOZE.js} +1 -1
  101. package/dist/assets/packet-7NZHBO7P-BpeiqkBZ.js +1 -0
  102. package/dist/assets/{pegDiagram-2B236MQR-D-U2KBGv.js → pegDiagram-2B236MQR-BEu_ZBjc.js} +1 -1
  103. package/dist/assets/pie-RZYD4A2V-Cy1yxsPd.js +1 -0
  104. package/dist/assets/{pieDiagram-ENE6RG2P-CBi3GusJ.js → pieDiagram-ENE6RG2P-CAwAwuBd.js} +1 -1
  105. package/dist/assets/plus-D6UN8XN5.js +1 -0
  106. package/dist/assets/{provider-scoped-model-input-CB-vojwC.js → provider-scoped-model-input-C1CUHVJx.js} +1 -1
  107. package/dist/assets/{providers-config-page-C80nti5H.js → providers-config-page-CMvI4R1e.js} +1 -1
  108. package/dist/assets/{quadrantDiagram-ABIIQ3AL-H0ZuvnZL.js → quadrantDiagram-ABIIQ3AL-CwE6F-_2.js} +1 -1
  109. package/dist/assets/radar-I7S5WNFK-CEcPKCj8.js +1 -0
  110. package/dist/assets/railroad-3IZDKUUU-CW38eNDs.js +1 -0
  111. package/dist/assets/railroad-abnf-AHOZXSZD-ByrylcXf.js +1 -0
  112. package/dist/assets/railroad-ebnf-EBAXGLYW-CNB6GwP-.js +1 -0
  113. package/dist/assets/railroad-peg-LSFZ7HO6-x6hnqlBG.js +1 -0
  114. package/dist/assets/{railroadDiagram-RFXS5EU6-D7Y2EqSd.js → railroadDiagram-RFXS5EU6-xgLekU3B.js} +1 -1
  115. package/dist/assets/react-7zG0fz1I.js +8 -0
  116. package/dist/assets/{refresh-cw-DZ_Y89ld.js → refresh-cw-BUg7hI3u.js} +1 -1
  117. package/dist/assets/remote-BYPq49kD.js +1 -0
  118. package/dist/assets/{requirementDiagram-TGXJPOKE-BiXNX1NN.js → requirementDiagram-TGXJPOKE-CyVUzEX5.js} +1 -1
  119. package/dist/assets/rotate-cw-B3qmXtfJ.js +1 -0
  120. package/dist/assets/runtime-config-page-DYuYv5yM.js +1 -0
  121. package/dist/assets/{sankeyDiagram-HTMAVEWB-qzDeLztW.js → sankeyDiagram-HTMAVEWB-JK6bFmYB.js} +1 -1
  122. package/dist/assets/{save-BgMfxs5u.js → save-CTCYuZqu.js} +1 -1
  123. package/dist/assets/{search-C7ruoV7u.js → search-CfFUpaf7.js} +1 -1
  124. package/dist/assets/{search-config-page-BTjfOZ6q.js → search-config-page-GUtGbk6i.js} +1 -1
  125. package/dist/assets/{secrets-config-page-BFjiSz73.js → secrets-config-page-Delo8ab0.js} +2 -2
  126. package/dist/assets/security-config-DoVoel5I.js +1 -0
  127. package/dist/assets/{select-BMX2ooE-.js → select-MSvkWmPP.js} +1 -1
  128. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BtRbPQtL.js → sequenceDiagram-DBY2YBRQ-BR5IokXV.js} +1 -1
  129. package/dist/assets/{settings-2-BNQF6hed.js → settings-2-DXUfAWgg.js} +1 -1
  130. package/dist/assets/{sizeCapture-X5ZJPWSS-CMYvM3SL.js → sizeCapture-X5ZJPWSS-SxG_YnMA.js} +1 -1
  131. package/dist/assets/skeleton-DDFivi9Y.js +1 -0
  132. package/dist/assets/{src-Bw-mzbQb.js → src-vgz8E_-O.js} +1 -1
  133. package/dist/assets/{stateDiagram-2N3HPSRC-BJ-mFeOz.js → stateDiagram-2N3HPSRC-DD4OoYhn.js} +1 -1
  134. package/dist/assets/stateDiagram-v2-6OUMAXLB-ZC6dFORv.js +1 -0
  135. package/dist/assets/{swimlanes-5IMT3BWC-BsucVOO2.js → swimlanes-5IMT3BWC-BWkFeyCT.js} +2 -2
  136. package/dist/assets/swimlanesDiagram-G3AALYLV-CRkh51Im.js +8 -0
  137. package/dist/assets/{tag-chip-BL-UXlVO.js → tag-chip-u8g0b0Nf.js} +1 -1
  138. package/dist/assets/{timeline-definition-FHXFAJF6-Bicewlzo.js → timeline-definition-FHXFAJF6-VTRPy6KA.js} +1 -1
  139. package/dist/assets/treeView-QDETBFTQ-Bw0DSCH7.js +1 -0
  140. package/dist/assets/treemap-6X3UGDF4-DFWBFcfh.js +1 -0
  141. package/dist/assets/{vennDiagram-L72KCM5P-BxOOnO9x.js → vennDiagram-L72KCM5P-Bvk42Pek.js} +1 -1
  142. package/dist/assets/wardley-OPB4EBWU-CoWturk0.js +1 -0
  143. package/dist/assets/{wardleyDiagram-EHGQE667-Cl7NltMv.js → wardleyDiagram-EHGQE667-DOFjx2s3.js} +1 -1
  144. package/dist/assets/x-DdOEUmR4.js +1 -0
  145. package/dist/assets/{xychartDiagram-FW5EYKEG-DiMIxQ7N.js → xychartDiagram-FW5EYKEG-DY5yRTUc.js} +1 -1
  146. package/dist/index.html +22 -21
  147. package/package.json +9 -9
  148. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +2 -0
  149. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
  150. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -0
  151. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +46 -0
  152. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +20 -0
  153. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +138 -54
  154. package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +25 -2
  155. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +46 -0
  156. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +2 -0
  157. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +52 -2
  158. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +97 -10
  159. package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +4 -0
  160. package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +10 -0
  161. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +27 -0
  162. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -1
  163. package/src/features/chat/features/workspace/components/__tests__/chat-session-child-sessions.test.tsx +51 -0
  164. package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx +90 -0
  165. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +9 -17
  166. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +4 -45
  167. package/src/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions.tsx +66 -0
  168. package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +171 -0
  169. package/src/features/chat/types/chat-message.types.ts +5 -0
  170. package/src/shared/components/doc-browser/__tests__/doc-browser-tab-strip.test.tsx +78 -0
  171. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +71 -3
  172. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +78 -1
  173. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +97 -44
  174. package/src/shared/lib/api/__tests__/ncp-session-query-cache.utils.test.ts +2 -0
  175. package/src/shared/lib/api/ncp-session-query-cache.utils.ts +4 -0
  176. package/src/shared/lib/api/ncp-session.types.ts +3 -0
  177. package/src/shared/lib/api/types.ts +1 -0
  178. package/src/shared/lib/api/utils/ncp-session.utils.ts +8 -0
  179. package/src/shared/lib/i18n/locales/en-US/chat.json +23 -0
  180. package/src/shared/lib/i18n/locales/zh-CN/chat.json +24 -1
  181. package/dist/assets/architecture-TIHT7OUA-DO2x53FK.js +0 -1
  182. package/dist/assets/channel-nBvMaVyP.js +0 -1
  183. package/dist/assets/chunk-2Q5K7J3B-CJgqFDS4.js +0 -1
  184. package/dist/assets/chunk-VAUOI2AC-BGvh3yIx.js +0 -1
  185. package/dist/assets/chunk-XXDRQBXY-DaBxG5gs.js +0 -1
  186. package/dist/assets/classDiagram-OUVF2IWQ-P6N03G2R.js +0 -1
  187. package/dist/assets/classDiagram-v2-EOCWNBFH-B1_uLvMv.js +0 -1
  188. package/dist/assets/cynefin-VYW2F7L2-B-H8Qp-n.js +0 -1
  189. package/dist/assets/doc-browser-BbirIYbj.js +0 -1
  190. package/dist/assets/doc-browser-Crqog5cn.js +0 -1
  191. package/dist/assets/eventmodeling-45OFAUF4-YVtShyFA.js +0 -1
  192. package/dist/assets/flowDiagram-23GEKE2U-BeKXabVO.js +0 -1
  193. package/dist/assets/gitGraph-TEB2WS4Q-jTSiBj-p.js +0 -1
  194. package/dist/assets/index-1f4Pv2q3.css +0 -1
  195. package/dist/assets/index-BYWLjYFQ.js +0 -109
  196. package/dist/assets/info-DKCQHKI2-BIporNWb.js +0 -1
  197. package/dist/assets/loader-circle-LVHz84Xj.js +0 -1
  198. package/dist/assets/mcp-marketplace-page-2SsrbaiQ.js +0 -1
  199. package/dist/assets/mcp-marketplace-page-XgYLOAkK.js +0 -9
  200. package/dist/assets/model-config-page-DL8ljJK-.js +0 -1
  201. package/dist/assets/packet-7NZHBO7P-CuFMADV0.js +0 -1
  202. package/dist/assets/pie-RZYD4A2V-XUeIXhM9.js +0 -1
  203. package/dist/assets/plus-BeGZHC3f.js +0 -1
  204. package/dist/assets/radar-I7S5WNFK-D-NNGh1k.js +0 -1
  205. package/dist/assets/railroad-3IZDKUUU-C67_uDaM.js +0 -1
  206. package/dist/assets/railroad-abnf-AHOZXSZD-BZ8Yflnd.js +0 -1
  207. package/dist/assets/railroad-ebnf-EBAXGLYW-B7zO7RoK.js +0 -1
  208. package/dist/assets/railroad-peg-LSFZ7HO6-R63PTgLz.js +0 -1
  209. package/dist/assets/react-DND3vEAM.js +0 -8
  210. package/dist/assets/remote-Codkr9SX.js +0 -1
  211. package/dist/assets/rotate-cw-CP3pQ4R8.js +0 -1
  212. package/dist/assets/runtime-config-page-Um1XDqXJ.js +0 -1
  213. package/dist/assets/security-config-Dx-wMb4q.js +0 -1
  214. package/dist/assets/skeleton-CWZ0IO2D.js +0 -1
  215. package/dist/assets/stateDiagram-v2-6OUMAXLB-BE-HqxOK.js +0 -1
  216. package/dist/assets/swimlanesDiagram-G3AALYLV-Dr1H9LJH.js +0 -8
  217. package/dist/assets/treeView-QDETBFTQ-npJ5dbqS.js +0 -1
  218. package/dist/assets/treemap-6X3UGDF4-pYrBx6oa.js +0 -1
  219. package/dist/assets/wardley-OPB4EBWU-CmSRKLR8.js +0 -1
  220. package/dist/assets/x-BSu1xwPK.js +0 -1
  221. /package/dist/assets/{array-BQVzeGRt.js → array-NADKd9aU.js} +0 -0
  222. /package/dist/assets/{chunk-KEIR6QF5-f9psyCXK.js → chunk-KEIR6QF5-BuyuzJJL.js} +0 -0
  223. /package/dist/assets/{chunk-Y2CYZVJY-D1mAU_Po.js → chunk-Y2CYZVJY-D_B2ZRNt.js} +0 -0
  224. /package/dist/assets/{config-hints-nV5HPOvq.js → config-hints-BcfVUMij.js} +0 -0
  225. /package/dist/assets/{cytoscape.esm-0uDtdD0l.js → cytoscape.esm-BANTlgza.js} +0 -0
  226. /package/dist/assets/{defaultLocale-DSWXGgt0.js → defaultLocale-C4_L0aEs.js} +0 -0
  227. /package/dist/assets/{dist--ta5rX12.js → dist-DAPEIJbz.js} +0 -0
  228. /package/dist/assets/{graphlib-CQ6QV4yP.js → graphlib-ChqXg0dI.js} +0 -0
  229. /package/dist/assets/{host-capabilities-Tk038i04.js → host-capabilities-D9C4CBK5.js} +0 -0
  230. /package/dist/assets/{init-Y6h9jGtZ.js → init-D8wZg9xq.js} +0 -0
  231. /package/dist/assets/{jszip.min-8hyJpgpx.js → jszip.min-CkbBfwAQ.js} +0 -0
  232. /package/dist/assets/{katex-ARhY9PS1.js → katex-TZJFa0J_.js} +0 -0
  233. /package/dist/assets/{middleware-DXsbf9xI.js → middleware-B3k-TSmZ.js} +0 -0
  234. /package/dist/assets/{path-10BjdyLh.js → path-DodfxcX7.js} +0 -0
  235. /package/dist/assets/{rough.esm-DxgC_14r.js → rough.esm-CUaa1Rbe.js} +0 -0
  236. /package/dist/assets/{xlsx-DrDuDNAa.js → xlsx-CxByi7bR.js} +0 -0
@@ -35,6 +35,33 @@ it('preserves NCP extension parts for the chat presentation adapter', () => {
35
35
  ]);
36
36
  });
37
37
 
38
+ it('preserves standard tool execution timing without reading opaque result timing', () => {
39
+ const adapted = adaptNcpMessageToUiMessage({
40
+ id: 'ncp-message-timing-1',
41
+ sessionId: 'ncp-session-1',
42
+ role: 'assistant',
43
+ status: 'streaming',
44
+ timestamp: '2026-08-14T00:00:00.000Z',
45
+ parts: [{
46
+ type: 'tool-invocation',
47
+ toolCallId: 'tool-timing-1',
48
+ toolName: 'exec',
49
+ state: 'call',
50
+ args: { command: 'pnpm test' },
51
+ result: { durationMs: 999_999 },
52
+ execution: { startedAt: '2026-08-14T00:00:01.000Z' },
53
+ }],
54
+ });
55
+
56
+ expect(adapted.parts[0]).toMatchObject({
57
+ type: 'tool-invocation',
58
+ toolInvocation: {
59
+ toolCallId: 'tool-timing-1',
60
+ execution: { startedAt: '2026-08-14T00:00:01.000Z' },
61
+ },
62
+ });
63
+ });
64
+
38
65
  describe('adaptNcpSessionSummary', () => {
39
66
  it('maps session metadata into shared session entry fields', () => {
40
67
  const adapted = adaptNcpSessionSummary(
@@ -284,7 +284,8 @@ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
284
284
  toolCallId: part.toolCallId ?? `${part.toolName}-${Math.random().toString(36).slice(2, 8)}`,
285
285
  toolName: part.toolName,
286
286
  args: stringifyUnknown(part.args),
287
- result: part.result
287
+ result: part.result,
288
+ ...(part.execution ? { execution: { ...part.execution } } : {})
288
289
  }
289
290
  });
290
291
  }
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { beforeEach, expect, it, vi } from 'vitest';
4
+ import { ChatSessionChildSessions } from '@/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions';
5
+ import type { ResolvedChildSessionTab } from '@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view';
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ openSideChatDraft: vi.fn(),
9
+ selectChildSessionDetail: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
13
+ usePresenter: () => ({ chatThreadManager: mocks }),
14
+ }));
15
+
16
+ beforeEach(() => vi.clearAllMocks());
17
+
18
+ function createChildSession(): ResolvedChildSessionTab {
19
+ return {
20
+ sessionKey: 'child-1',
21
+ parentSessionKey: 'parent-1',
22
+ title: 'Research branch',
23
+ agentId: 'main',
24
+ updatedAt: null,
25
+ lastMessageAt: null,
26
+ readAt: null,
27
+ runStatus: undefined,
28
+ sessionTypeLabel: 'Native',
29
+ preferredModel: 'openai/gpt-5',
30
+ projectName: 'nextbot',
31
+ projectRoot: '/tmp/nextbot',
32
+ };
33
+ }
34
+
35
+ it('opens a new child-session draft from the management page', async () => {
36
+ const user = userEvent.setup();
37
+ render(<ChatSessionChildSessions childSessionTabs={[]} sessionKey="parent-1" />);
38
+
39
+ expect(screen.getByText('No child sessions yet.')).toBeTruthy();
40
+ await user.click(screen.getByRole('button', { name: 'New child session' }));
41
+ expect(mocks.openSideChatDraft).toHaveBeenCalledWith('parent-1');
42
+ });
43
+
44
+ it('keeps existing child sessions selectable beside the create action', async () => {
45
+ const user = userEvent.setup();
46
+ render(<ChatSessionChildSessions childSessionTabs={[createChildSession()]} sessionKey="parent-1" />);
47
+
48
+ expect(screen.getByRole('button', { name: 'New child session' })).toBeTruthy();
49
+ await user.click(screen.getByRole('button', { name: /Research branch/ }));
50
+ expect(mocks.selectChildSessionDetail).toHaveBeenCalledWith('child-1');
51
+ });
@@ -0,0 +1,90 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { beforeEach, expect, it, vi } from 'vitest';
4
+ import { ChatSessionTokenUsage } from '@/features/chat/features/workspace/components/overview/chat-session-token-usage';
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ refetch: vi.fn(),
8
+ query: {
9
+ data: undefined as unknown,
10
+ isLoading: false,
11
+ isError: false,
12
+ },
13
+ }));
14
+
15
+ vi.mock('@/features/chat/features/ncp/hooks/use-ncp-session-queries', () => ({
16
+ useNcpSessionTokenUsage: () => ({
17
+ ...mocks.query,
18
+ refetch: mocks.refetch,
19
+ }),
20
+ }));
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ mocks.refetch.mockResolvedValue(undefined);
25
+ mocks.query.data = {
26
+ sessionId: 'parent-1',
27
+ totals: {
28
+ inputTokens: 1_250,
29
+ outputTokens: 240,
30
+ cachedInputTokens: 500,
31
+ totalTokens: 1_490,
32
+ cacheHitRate: 0.4,
33
+ },
34
+ models: [
35
+ {
36
+ model: 'openai/gpt-5',
37
+ inputTokens: 1_000,
38
+ outputTokens: 200,
39
+ cachedInputTokens: 400,
40
+ totalTokens: 1_200,
41
+ cacheHitRate: 0.4,
42
+ runCount: 2,
43
+ modelCallCount: 3,
44
+ reportedModelCallCount: 3,
45
+ status: 'reported',
46
+ },
47
+ {
48
+ model: 'anthropic/claude-sonnet-4',
49
+ inputTokens: 250,
50
+ outputTokens: 40,
51
+ cachedInputTokens: 100,
52
+ totalTokens: 290,
53
+ cacheHitRate: 0.4,
54
+ runCount: 1,
55
+ modelCallCount: 2,
56
+ reportedModelCallCount: 1,
57
+ status: 'partial',
58
+ },
59
+ ],
60
+ runCount: 3,
61
+ modelCallCount: 5,
62
+ reportedModelCallCount: 4,
63
+ status: 'partial',
64
+ };
65
+ mocks.query.isLoading = false;
66
+ mocks.query.isError = false;
67
+ });
68
+
69
+ it('shows totals, cache hit rate, and usage grouped by model', () => {
70
+ render(<ChatSessionTokenUsage sessionKey="parent-1" />);
71
+
72
+ expect(screen.getByText('Token usage')).toBeTruthy();
73
+ expect(screen.getByText('1,490')).toBeTruthy();
74
+ expect(screen.getAllByText('40%').length).toBeGreaterThan(0);
75
+ expect(screen.getByText('openai/gpt-5')).toBeTruthy();
76
+ expect(screen.getByText('anthropic/claude-sonnet-4')).toBeTruthy();
77
+ expect(screen.getAllByText('Partial usage')).toHaveLength(2);
78
+ });
79
+
80
+ it('lets users retry after token usage fails to load', async () => {
81
+ const user = userEvent.setup();
82
+ mocks.query.data = undefined;
83
+ mocks.query.isError = true;
84
+
85
+ render(<ChatSessionTokenUsage sessionKey="parent-1" />);
86
+
87
+ expect(screen.getByRole('alert').textContent).toContain('Couldn’t load token usage.');
88
+ await user.click(screen.getByRole('button', { name: 'Retry' }));
89
+ expect(mocks.refetch).toHaveBeenCalledTimes(1);
90
+ });
@@ -78,6 +78,10 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
78
78
  }),
79
79
  }));
80
80
 
81
+ vi.mock('@/features/chat/features/workspace/components/overview/chat-session-token-usage', () => ({
82
+ ChatSessionTokenUsage: () => <section><h3>Token usage</h3></section>,
83
+ }));
84
+
81
85
  vi.mock('@/shared/lib/api', async (importOriginal) => ({
82
86
  ...(await importOriginal()),
83
87
  createServerPathDirectory: mocks.createServerPathDirectory,
@@ -250,41 +254,29 @@ it('shows all session workspace entries in the overview', async () => {
250
254
  );
251
255
 
252
256
  expect(screen.getByText('Overview')).toBeTruthy();
257
+ expect(screen.getByText('Token usage')).toBeTruthy();
253
258
  const childSessionsButton = screen.getByRole('button', {
254
259
  name: /Child sessions/,
255
260
  });
256
261
  const cronJobsButton = screen.getByRole('button', {
257
262
  name: /Scheduled tasks/,
258
263
  });
264
+ const projectFilesButton = screen.getByRole('button', { name: /Project files/ });
259
265
 
260
266
  expect((childSessionsButton as HTMLButtonElement).disabled).toBe(false);
261
267
  expect((cronJobsButton as HTMLButtonElement).disabled).toBe(false);
268
+ expect(projectFilesButton.compareDocumentPosition(screen.getByText('Token usage')) & Node.DOCUMENT_POSITION_FOLLOWING)
269
+ .toBeTruthy();
262
270
 
263
271
  await user.click(childSessionsButton);
264
272
  await user.click(cronJobsButton);
265
- await user.click(screen.getByRole('button', { name: /Project files/ }));
273
+ await user.click(projectFilesButton);
266
274
 
267
275
  expect(mocks.openChildSessions).toHaveBeenCalledWith('parent-1');
268
276
  expect(mocks.openSessionCronPanel).toHaveBeenCalledWith('parent-1');
269
277
  expect(mocks.openProjectFiles).toHaveBeenCalledWith('parent-1');
270
278
  });
271
279
 
272
- it('shows an empty child sessions page instead of disabling the entry', () => {
273
- render(
274
- <ChatSessionWorkspacePanelContent
275
- activeSelection={{ kind: 'child-sessions' }}
276
- childSessionTabs={[]}
277
- filePreviewRefreshVersion={0}
278
- sessionKey="parent-1"
279
- sessionCronJobs={[]}
280
- sessionProjectRoot={null}
281
- sessionWorkingDir={null}
282
- />,
283
- );
284
-
285
- expect(screen.getByText('No child sessions yet.')).toBeTruthy();
286
- });
287
-
288
280
  it('shows the selected session project as a hierarchical file tree', () => {
289
281
  render(
290
282
  <ChatSessionWorkspacePanelContent
@@ -15,6 +15,8 @@ import { SessionConversationArea } from '@/features/chat/features/conversation/c
15
15
  import { ChatSessionWorkspaceFilePreview } from '@/features/chat/features/workspace/components/chat-session-workspace-file-preview';
16
16
  import { ChatSessionWorkspaceDirectoryBrowser } from '@/features/chat/features/workspace/components/chat-session-workspace-directory-browser';
17
17
  import { SessionCronJobContent } from '@/features/chat/features/workspace/components/session-cron-job-content';
18
+ import { ChatSessionChildSessions } from '@/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions';
19
+ import { ChatSessionTokenUsage } from '@/features/chat/features/workspace/components/overview/chat-session-token-usage';
18
20
  import type { ResolvedChildSessionTab } from '@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view';
19
21
  import type { WorkspaceSelection } from '@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils';
20
22
  import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
@@ -126,50 +128,7 @@ function WorkspaceOverview({
126
128
  }}
127
129
  />
128
130
  </div>
129
- </div>
130
- </div>
131
- );
132
- }
133
-
134
- function WorkspaceChildSessions({ childSessionTabs }: { childSessionTabs: readonly ResolvedChildSessionTab[] }) {
135
- const presenter = usePresenter();
136
-
137
- return (
138
- <div className="h-full overflow-auto bg-gray-50/45 px-4 py-5 custom-scrollbar">
139
- <div className="mx-auto max-w-xl">
140
- <div className="flex items-center gap-2">
141
- <h2 className="text-base font-semibold text-gray-900">{t('chatWorkspaceChildSessions')}</h2>
142
- <span className="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-gray-500">
143
- {childSessionTabs.length}
144
- </span>
145
- </div>
146
- {childSessionTabs.length === 0 ? (
147
- <div className="py-10 text-center text-sm text-gray-500">{t('chatWorkspaceChildSessionsEmpty')}</div>
148
- ) : (
149
- <div className="mt-4 space-y-2">
150
- {childSessionTabs.map((tab) => (
151
- <button
152
- key={tab.sessionKey}
153
- type="button"
154
- className="group flex w-full items-center gap-3 rounded-lg border border-gray-200/80 bg-white px-3 py-3 text-left transition-colors hover:border-gray-300 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
155
- onClick={() => presenter.chatThreadManager.selectChildSessionDetail(tab.sessionKey)}
156
- >
157
- <span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-600">
158
- <GitBranch className="h-4 w-4" />
159
- </span>
160
- <span className="min-w-0 flex-1">
161
- <span className="block truncate text-sm font-medium text-gray-900">{tab.title}</span>
162
- {tab.projectName || tab.sessionTypeLabel ? (
163
- <span className="mt-0.5 block truncate text-xs text-gray-500">
164
- {[tab.sessionTypeLabel, tab.projectName].filter(Boolean).join(' · ')}
165
- </span>
166
- ) : null}
167
- </span>
168
- <ChevronRight className="h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-gray-500" />
169
- </button>
170
- ))}
171
- </div>
172
- )}
131
+ <ChatSessionTokenUsage sessionKey={sessionKey} />
173
132
  </div>
174
133
  </div>
175
134
  );
@@ -299,7 +258,7 @@ function WorkspaceSelectedContent({
299
258
  }
300
259
 
301
260
  if (activeSelection.kind === 'child-sessions') {
302
- return <WorkspaceChildSessions childSessionTabs={childSessionTabs} />;
261
+ return <ChatSessionChildSessions childSessionTabs={childSessionTabs} sessionKey={sessionKey} />;
303
262
  }
304
263
 
305
264
  if (activeSelection.kind === 'side-chat-draft') {
@@ -0,0 +1,66 @@
1
+ import { ChevronRight, GitBranch, MessageSquarePlus } from 'lucide-react';
2
+
3
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
4
+ import type { ResolvedChildSessionTab } from '@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view';
5
+ import { t } from '@/shared/lib/i18n';
6
+
7
+ export function ChatSessionChildSessions({
8
+ childSessionTabs,
9
+ sessionKey,
10
+ }: {
11
+ childSessionTabs: readonly ResolvedChildSessionTab[];
12
+ sessionKey: string | null;
13
+ }) {
14
+ const presenter = usePresenter();
15
+
16
+ return (
17
+ <div className="h-full overflow-auto bg-gray-50/45 px-4 py-5 custom-scrollbar">
18
+ <div className="mx-auto max-w-xl">
19
+ <div className="flex items-center justify-between gap-3">
20
+ <div className="flex min-w-0 items-center gap-2">
21
+ <h2 className="truncate text-base font-semibold text-gray-900">{t('chatWorkspaceChildSessions')}</h2>
22
+ <span className="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-gray-500">
23
+ {childSessionTabs.length}
24
+ </span>
25
+ </div>
26
+ <button
27
+ type="button"
28
+ className="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-2.5 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25 disabled:cursor-not-allowed disabled:opacity-50"
29
+ disabled={!sessionKey}
30
+ onClick={() => presenter.chatThreadManager.openSideChatDraft(sessionKey)}
31
+ >
32
+ <MessageSquarePlus className="h-3.5 w-3.5" />
33
+ <span>{t('chatWorkspaceCreateChildSession')}</span>
34
+ </button>
35
+ </div>
36
+ {childSessionTabs.length === 0 ? (
37
+ <div className="py-10 text-center text-sm text-gray-500">{t('chatWorkspaceChildSessionsEmpty')}</div>
38
+ ) : (
39
+ <div className="mt-4 space-y-2">
40
+ {childSessionTabs.map((tab) => (
41
+ <button
42
+ key={tab.sessionKey}
43
+ type="button"
44
+ className="group flex w-full items-center gap-3 rounded-lg border border-gray-200/80 bg-white px-3 py-3 text-left transition-colors hover:border-gray-300 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
45
+ onClick={() => presenter.chatThreadManager.selectChildSessionDetail(tab.sessionKey)}
46
+ >
47
+ <span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-600">
48
+ <GitBranch className="h-4 w-4" />
49
+ </span>
50
+ <span className="min-w-0 flex-1">
51
+ <span className="block truncate text-sm font-medium text-gray-900">{tab.title}</span>
52
+ {tab.projectName || tab.sessionTypeLabel ? (
53
+ <span className="mt-0.5 block truncate text-xs text-gray-500">
54
+ {[tab.sessionTypeLabel, tab.projectName].filter(Boolean).join(' · ')}
55
+ </span>
56
+ ) : null}
57
+ </span>
58
+ <ChevronRight className="h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-gray-500" />
59
+ </button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </div>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,171 @@
1
+ import { ChartNoAxesColumn } from 'lucide-react';
2
+
3
+ import { useNcpSessionTokenUsage } from '@/features/chat/features/ncp/hooks/use-ncp-session-queries';
4
+ import { t } from '@/shared/lib/i18n';
5
+ import { cn } from '@/shared/lib/utils';
6
+
7
+ function formatTokenCount(value: number | null): string {
8
+ return value === null ? '—' : new Intl.NumberFormat().format(value);
9
+ }
10
+
11
+ function formatCacheHitRate(value: number | null): string {
12
+ return value === null
13
+ ? '—'
14
+ : new Intl.NumberFormat(undefined, {
15
+ style: 'percent',
16
+ maximumFractionDigits: 1,
17
+ }).format(value);
18
+ }
19
+
20
+ function TokenUsageStatusBadge({ status }: { status: 'reported' | 'partial' | 'unavailable' }) {
21
+ return (
22
+ <span
23
+ className={cn(
24
+ 'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-semibold',
25
+ status === 'reported'
26
+ ? 'bg-emerald-50 text-emerald-700'
27
+ : status === 'partial'
28
+ ? 'bg-amber-50 text-amber-700'
29
+ : 'bg-gray-100 text-gray-500',
30
+ )}
31
+ >
32
+ {t(
33
+ status === 'reported'
34
+ ? 'chatWorkspaceTokenUsageReported'
35
+ : status === 'partial'
36
+ ? 'chatWorkspaceTokenUsagePartial'
37
+ : 'chatWorkspaceTokenUsageUnavailable',
38
+ )}
39
+ </span>
40
+ );
41
+ }
42
+
43
+ function TokenUsageMetric({ label, value }: { label: string; value: string }) {
44
+ return (
45
+ <div className="min-w-0 rounded-md bg-gray-50 px-2.5 py-2">
46
+ <div className="truncate text-[10px] font-medium text-gray-500">{label}</div>
47
+ <div className="mt-0.5 truncate text-sm font-semibold tabular-nums text-gray-900">
48
+ {value}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function ChatSessionTokenUsage({ sessionKey }: { sessionKey: string | null }) {
55
+ const usageQuery = useNcpSessionTokenUsage(sessionKey);
56
+ const usage = usageQuery.data;
57
+
58
+ return (
59
+ <section
60
+ aria-labelledby="chat-workspace-token-usage-title"
61
+ className="mt-4 rounded-lg border border-gray-200/80 bg-white p-3"
62
+ >
63
+ <div className="flex items-start gap-3">
64
+ <span className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-600">
65
+ <ChartNoAxesColumn className="h-4 w-4" />
66
+ </span>
67
+ <div className="min-w-0 flex-1">
68
+ <div className="flex items-center justify-between gap-2">
69
+ <h3 id="chat-workspace-token-usage-title" className="text-sm font-semibold text-gray-900">
70
+ {t('chatWorkspaceTokenUsage')}
71
+ </h3>
72
+ {usage ? <TokenUsageStatusBadge status={usage.status} /> : null}
73
+ </div>
74
+ <p className="mt-0.5 text-xs leading-5 text-gray-500">
75
+ {t('chatWorkspaceTokenUsageDescription')}
76
+ </p>
77
+ </div>
78
+ </div>
79
+
80
+ {usageQuery.isLoading ? (
81
+ <p className="py-5 text-center text-xs text-gray-500" aria-live="polite">
82
+ {t('chatWorkspaceTokenUsageLoading')}
83
+ </p>
84
+ ) : usageQuery.isError ? (
85
+ <div className="flex items-center justify-between gap-3 py-4 text-xs text-rose-600" role="alert">
86
+ <span>{t('chatWorkspaceTokenUsageLoadFailed')}</span>
87
+ <button
88
+ type="button"
89
+ className="shrink-0 rounded px-1.5 py-1 font-semibold text-gray-700 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
90
+ onClick={() => void usageQuery.refetch()}
91
+ >
92
+ {t('chatWorkspaceTokenUsageRetry')}
93
+ </button>
94
+ </div>
95
+ ) : usage && usage.runCount > 0 ? (
96
+ <div className="mt-3 space-y-3">
97
+ <div className="grid grid-cols-2 gap-1.5">
98
+ <TokenUsageMetric
99
+ label={t('chatWorkspaceTokenUsageTotal')}
100
+ value={formatTokenCount(usage.totals.totalTokens)}
101
+ />
102
+ <TokenUsageMetric
103
+ label={t('chatWorkspaceTokenUsageInput')}
104
+ value={formatTokenCount(usage.totals.inputTokens)}
105
+ />
106
+ <TokenUsageMetric
107
+ label={t('chatWorkspaceTokenUsageOutput')}
108
+ value={formatTokenCount(usage.totals.outputTokens)}
109
+ />
110
+ <TokenUsageMetric
111
+ label={t('chatWorkspaceTokenUsageCachedInput')}
112
+ value={formatTokenCount(usage.totals.cachedInputTokens)}
113
+ />
114
+ <TokenUsageMetric
115
+ label={t('chatWorkspaceTokenUsageCacheHitRate')}
116
+ value={formatCacheHitRate(usage.totals.cacheHitRate)}
117
+ />
118
+ <TokenUsageMetric
119
+ label={t('chatWorkspaceTokenUsageRuns')}
120
+ value={String(usage.runCount)}
121
+ />
122
+ </div>
123
+ <div>
124
+ <div className="mb-1.5 text-[11px] font-medium text-gray-700">
125
+ {t('chatWorkspaceTokenUsageByModel')}
126
+ </div>
127
+ <div className="space-y-1.5">
128
+ {usage.models.map((modelUsage) => (
129
+ <div key={modelUsage.model} className="rounded-md border border-gray-200/70 px-2.5 py-2">
130
+ <div className="flex min-w-0 items-center justify-between gap-2">
131
+ <span className="min-w-0 truncate text-xs font-semibold text-gray-900" title={modelUsage.model}>
132
+ {modelUsage.model}
133
+ </span>
134
+ <TokenUsageStatusBadge status={modelUsage.status} />
135
+ </div>
136
+ <div className="mt-1 text-[10px] text-gray-500">
137
+ {t('chatWorkspaceTokenUsageModelRunCount').replace('{count}', String(modelUsage.runCount))}
138
+ {' · '}
139
+ {t('chatWorkspaceTokenUsageModelCacheHitRate').replace(
140
+ '{rate}',
141
+ formatCacheHitRate(modelUsage.cacheHitRate),
142
+ )}
143
+ </div>
144
+ <dl className="mt-2 grid grid-cols-2 gap-x-3 gap-y-1 text-[10px] sm:grid-cols-4">
145
+ {[
146
+ [t('chatWorkspaceTokenUsageInput'), modelUsage.inputTokens],
147
+ [t('chatWorkspaceTokenUsageOutput'), modelUsage.outputTokens],
148
+ [t('chatWorkspaceTokenUsageCachedInput'), modelUsage.cachedInputTokens],
149
+ [t('chatWorkspaceTokenUsageTotal'), modelUsage.totalTokens],
150
+ ].map(([label, value]) => (
151
+ <div key={String(label)} className="flex min-w-0 items-center justify-between gap-1 sm:block">
152
+ <dt className="truncate text-gray-500">{label}</dt>
153
+ <dd className="shrink-0 font-semibold tabular-nums text-gray-800 sm:mt-0.5">
154
+ {formatTokenCount(value as number | null)}
155
+ </dd>
156
+ </div>
157
+ ))}
158
+ </dl>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ ) : (
165
+ <p className="py-5 text-center text-xs leading-5 text-gray-500">
166
+ {t('chatWorkspaceTokenUsageEmpty')}
167
+ </p>
168
+ )}
169
+ </section>
170
+ );
171
+ }
@@ -55,6 +55,11 @@ export type ChatMessagePartSource =
55
55
  error?: string;
56
56
  cancelled?: boolean;
57
57
  toolCallId?: string;
58
+ execution?: {
59
+ startedAt?: string;
60
+ endedAt?: string;
61
+ durationMs?: number;
62
+ };
58
63
  };
59
64
  }
60
65
  | {
@@ -0,0 +1,78 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { DocBrowserTabStrip } from "@/shared/components/doc-browser/doc-browser-tab-strip";
4
+ import type { DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
5
+
6
+ const docsTab: DocBrowserTab = {
7
+ id: "docs",
8
+ kind: "docs",
9
+ title: "Docs",
10
+ currentUrl: "nextclaw://docs",
11
+ history: ["nextclaw://docs"],
12
+ historyIndex: 0,
13
+ navVersion: 0,
14
+ };
15
+
16
+ function renderTabStrip(tabs: DocBrowserTab[]) {
17
+ return render(
18
+ <DocBrowserTabStrip
19
+ tabs={tabs}
20
+ activeTabId={tabs[0]?.id ?? ""}
21
+ canGoBack={false}
22
+ canGoForward={false}
23
+ isDocked={true}
24
+ isFullscreen={false}
25
+ onGoBack={vi.fn()}
26
+ onGoForward={vi.fn()}
27
+ onOpenNewTab={vi.fn()}
28
+ onSetActiveTab={vi.fn()}
29
+ onCloseTab={vi.fn()}
30
+ onClose={vi.fn()}
31
+ onDragStart={vi.fn()}
32
+ onToggleMode={vi.fn()}
33
+ />,
34
+ );
35
+ }
36
+
37
+ describe("DocBrowserTabStrip", () => {
38
+ it("keeps content-sized tabs and renders each tab's own icon", () => {
39
+ renderTabStrip([
40
+ docsTab,
41
+ {
42
+ ...docsTab,
43
+ id: "marketplace",
44
+ kind: "content",
45
+ title: "Marketplace",
46
+ currentUrl: "nextclaw://marketplace",
47
+ dockIcon: { type: "url", url: "/marketplace-icon.png" },
48
+ },
49
+ ]);
50
+
51
+ expect(
52
+ screen.getByRole("button", { name: "Docs" })
53
+ .querySelector(".lucide-book-open"),
54
+ ).toBeTruthy();
55
+ expect(
56
+ screen.getByRole("button", { name: "Marketplace" })
57
+ .querySelector('img[src="/marketplace-icon.png"]'),
58
+ ).toBeTruthy();
59
+
60
+ const marketplaceTab = screen.getByRole("button", { name: "Marketplace" })
61
+ .closest("[data-compact-tab-item]");
62
+ expect(marketplaceTab?.className).toContain("max-w-[220px]");
63
+ expect(marketplaceTab?.className).not.toContain("min-w-[");
64
+
65
+ fireEvent.error(
66
+ screen.getByRole("button", { name: "Marketplace" })
67
+ .querySelector('img[src="/marketplace-icon.png"]')!,
68
+ );
69
+ expect(
70
+ screen.getByRole("button", { name: "Marketplace" })
71
+ .querySelector(".lucide-app-window"),
72
+ ).toBeTruthy();
73
+ expect(
74
+ screen.getByRole("button", { name: "Marketplace" })
75
+ .querySelector("img"),
76
+ ).toBeNull();
77
+ });
78
+ });