@nextclaw/ui 0.17.1 → 0.17.2
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.
- package/CHANGELOG.md +20 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-BcFJp_1X.js → abnfDiagram-VRR7QNED-DvTjgVMf.js} +1 -1
- package/dist/assets/{aiden0z-pptx-renderer.es-Dp2ezlQn.js → aiden0z-pptx-renderer.es-C1nfBAXR.js} +1 -1
- package/dist/assets/{api-DJ6ZKWFY.js → api-CuPWhNDM.js} +3 -3
- package/dist/assets/{appearance-settings-page-B-juoJ6F.js → appearance-settings-page-DRjg6UP3.js} +1 -1
- package/dist/assets/{arc-CkgyPMyW.js → arc-BvhODBXv.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-Bjp9jf0h.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-CMd4cIjI.js → architectureDiagram-ZJ3FMSHR-BKO9RgNS.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-Bz3tbhet.js → blockDiagram-677ZJIJ3-DHpDXy7D.js} +1 -1
- package/dist/assets/book-open-B4ovWX14.js +1 -0
- package/dist/assets/{c4Diagram-LMCZKHZV-D9hFNAGv.js → c4Diagram-LMCZKHZV-BG_iQnDr.js} +1 -1
- package/dist/assets/channel-5T3pdR-s.js +1 -0
- package/dist/assets/{channels-list-page-mVHYa7xW.js → channels-list-page-B26GXq63.js} +2 -2
- package/dist/assets/{chat-page-DsMYGNee.js → chat-page-QA0dpNyU.js} +1 -1
- package/dist/assets/chunk-2Q5K7J3B-DTZ3tGfE.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-7NrjEITq.js → chunk-32BRIVSS-DR9M0LLx.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-viIcqj9q.js → chunk-52WLFC77-698iC3BB.js} +1 -1
- package/dist/assets/{chunk-5FCAYU7R-Vsu4J-VM.js → chunk-5FCAYU7R-DikTj9Ey.js} +1 -1
- package/dist/assets/{chunk-5HE753X5-C-w6CQZK.js → chunk-5HE753X5-ko0o7_q0.js} +1 -1
- package/dist/assets/{chunk-5JV3BV7I-CuKgJQ4W.js → chunk-5JV3BV7I-CEHufhgo.js} +1 -1
- package/dist/assets/{chunk-5TONJI2A-CYeiWZKb.js → chunk-5TONJI2A-CGgno1cu.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DRCbfwIn.js → chunk-5VM5RSS4-DkiB4cUQ.js} +1 -1
- package/dist/assets/{chunk-7BUUIJ7U-BvydOcm3.js → chunk-7BUUIJ7U-DzptcytQ.js} +1 -1
- package/dist/assets/{chunk-BIQX33UG-RlppOXKo.js → chunk-BIQX33UG-DMRSQJvT.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-DAizzYwr.js → chunk-C7G6YPKG-Bw1BwwuY.js} +1 -1
- package/dist/assets/{chunk-CQNSW5MT-5eChJHca.js → chunk-CQNSW5MT-C0z6eHBk.js} +1 -1
- package/dist/assets/{chunk-CYSBUYHQ-YHPr53fv.js → chunk-CYSBUYHQ-ByPyAouO.js} +1 -1
- package/dist/assets/{chunk-EMLP6XTP-CK4D7Hz5.js → chunk-EMLP6XTP-CJlAXWZg.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BmlDFIRW.js → chunk-EX3LRPZG-Dqq4zKJu.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-D0ksHZZ9.js → chunk-FWX5IMBZ-B0SNoCmi.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-DPkq8FCm.js → chunk-HOUHSVGY-DG_GJih_.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-FAatv5fC.js → chunk-ICXQ74PX-Drxk_6bE.js} +1 -1
- package/dist/assets/{chunk-JG7HCLWE-DLkIi4F7.js → chunk-JG7HCLWE-DdLFPpnd.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DawOjNE3.js → chunk-JWPE2WC7-B8M4G76F.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-Di52TorW.js → chunk-MOJQB5TN-XP6FJHY_.js} +1 -1
- package/dist/assets/{chunk-MOZMSUNE-PfyK9w1-.js → chunk-MOZMSUNE-C_6x18Qw.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-C3jdH3Ty.js → chunk-OGEWGWER-N25BzCmG.js} +1 -1
- package/dist/assets/{chunk-OSBZ3O6U-CecHo-f0.js → chunk-OSBZ3O6U-ASDEm7y0.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-f3IJh3sN.js → chunk-PUDLZKDR-CTlvAcn7.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-DQFeUPxZ.js → chunk-Q4XR5HBZ-Da9BWfB6.js} +1 -1
- package/dist/assets/{chunk-QBLGF6JB-BhUeG0y2.js → chunk-QBLGF6JB-Bi6RpTJO.js} +1 -1
- package/dist/assets/{chunk-R7FJI6CG-C3XpZ-2u.js → chunk-R7FJI6CG-DkMsSPvK.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CAqa5klQ.js → chunk-RYQCIY6F-DZp0eU-j.js} +1 -1
- package/dist/assets/{chunk-U6XO7XAA-D_WXezeD.js → chunk-U6XO7XAA-vfinDSrj.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CZ70oOtp.js → chunk-V7JOEXUC-C9Nksccs.js} +1 -1
- package/dist/assets/chunk-VAUOI2AC-HLGIHQ6k.js +1 -0
- package/dist/assets/{chunk-VR4S4FIN-B7AHEYYk.js → chunk-VR4S4FIN-Xv6zfwXs.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-Dn9To_OI.js → chunk-WYO6CB5R-mOJNzbWl.js} +2 -2
- package/dist/assets/chunk-XXDRQBXY-Dl0m6Ni-.js +1 -0
- package/dist/assets/{chunk-YOTPTUD7-BYEgRBk3.js → chunk-YOTPTUD7-1-aKSu4o.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-C6YtZDU0.js → chunk-ZGVPDNZ5-OJ8veMpK.js} +1 -1
- package/dist/assets/{chunk-ZIRB5QZD-DMuYtnHF.js → chunk-ZIRB5QZD-SR_Nq_PP.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DcDnzGNX.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BKXKMzKM.js +1 -0
- package/dist/assets/{config-split-page-BaqLcRxk.js → config-split-page-Box_-aBK.js} +1 -1
- package/dist/assets/{confirm-dialog-DasFoaCT.js → confirm-dialog-uqcCxomz.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Bc2Ex5Y3.js → cose-bilkent-JH36ORCC-NjVgz2Bw.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-BWg7U2UD.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DVysQoGz.js → cynefinDiagram-TSTJHNR4-B_17LDqD.js} +1 -1
- package/dist/assets/{dagre-CMhQ1cAG.js → dagre-CMe101gk.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-RYyWpPJZ.js → dagre-VKFMJZFB-qo8nLb3B.js} +1 -1
- package/dist/assets/{desktop-update-config-CoEvKrpB.js → desktop-update-config-DkDor159.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BD6KONdF.js → diagram-FQU43EPY-Ba3XIklC.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-CGB2ptRR.js → diagram-G47NLZAW-CG45TcUx.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-DnkOI6cU.js → diagram-NH7WQ7WH-2F2HrwV-.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DF82RZ7P.js → diagram-OA4YK3LP-CXfWH9NZ.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-Be2v7Ck9.js → diagram-WEI45ONY-CH_4yepc.js} +1 -1
- package/dist/assets/{dist-C741xUwt.js → dist-CZLIZntJ.js} +1 -1
- package/dist/assets/{dist-zOKNc7QQ.js → dist-Co2G-YjN.js} +1 -1
- package/dist/assets/doc-browser-CbCBDdap.js +1 -0
- package/dist/assets/doc-browser-DUqckL2E.js +1 -0
- package/dist/assets/{doc-browser-context-BT8NWQyM.js → doc-browser-context-aeZu0FHR.js} +1 -1
- package/dist/assets/{docx-preview-BqwAjrj3.js → docx-preview-CoM4JK7_.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CP98OYk8.js → ebnfDiagram-CCIWWBDH-BwiG4Ea1.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-ZLlx8xfs.js → erDiagram-Q63AITRT-BML3hpnQ.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-SB-J4tpq.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-BQ_u-eLM.js +1 -0
- package/dist/assets/{form-actions-BcukaMyR.js → form-actions-C3Pa1HQb.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-DXCDjHR2.js → ganttDiagram-NO4QXBWP-80enfWbD.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-DPxNwsFd.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-l2HWrTJp.js → gitGraphDiagram-IHSO6WYX-CVLqXr-b.js} +1 -1
- package/dist/assets/index-Bsoscjk7.css +1 -0
- package/dist/assets/index-CCWgeWoR.js +109 -0
- package/dist/assets/info-DKCQHKI2-DNlNGH90.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-D-DjX-bn.js → infoDiagram-FWYZ7A6U-Dyvs2t0i.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-y_Xy128F.js → ishikawaDiagram-FXEZZL3T-sSnxDKfh.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-DJn5xYcQ.js → journeyDiagram-5HDEW3XC-D22V71Mf.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DJVIOR21.js → kanban-definition-HUTT4EX6-D3ds0p-g.js} +1 -1
- package/dist/assets/{line-Cos7PIOj.js → line-CbyFs065.js} +1 -1
- package/dist/assets/{linear-BtDLqtzF.js → linear-4DKtnZBS.js} +1 -1
- package/dist/assets/loader-circle-DfbWUPdI.js +1 -0
- package/dist/assets/{map-BKnQyvd0.js → map-CcZzEoz6.js} +1 -1
- package/dist/assets/mcp-marketplace-page-B2D7ciJL.js +1 -0
- package/dist/assets/mcp-marketplace-page-BmqbIQQQ.js +9 -0
- package/dist/assets/{mermaid-parser.core-XUNZhr2W.js → mermaid-parser.core-DPmMOjXD.js} +2 -2
- package/dist/assets/{mermaid.core-ySrvolIe.js → mermaid.core-BSf-ddRa.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-BypL9tei.js → mindmap-definition-LN4V7U3C-BUTwWPyx.js} +1 -1
- package/dist/assets/model-config-page-BXUoZEEZ.js +1 -0
- package/dist/assets/{navigation-link-BoMNAKyw.js → navigation-link-DngetDmN.js} +1 -1
- package/dist/assets/{ordinal-CxxVWxYU.js → ordinal-BgLYtOZE.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-BpeiqkBZ.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-D-U2KBGv.js → pegDiagram-2B236MQR-BEu_ZBjc.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-Cy1yxsPd.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-CBi3GusJ.js → pieDiagram-ENE6RG2P-CAwAwuBd.js} +1 -1
- package/dist/assets/plus-wNpOxBfP.js +1 -0
- package/dist/assets/{provider-scoped-model-input-CB-vojwC.js → provider-scoped-model-input-C4a2TuV7.js} +1 -1
- package/dist/assets/{providers-config-page-C80nti5H.js → providers-config-page-CHSka1kQ.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-H0ZuvnZL.js → quadrantDiagram-ABIIQ3AL-CwE6F-_2.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-CEcPKCj8.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-CW38eNDs.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-ByrylcXf.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-CNB6GwP-.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-x6hnqlBG.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6-D7Y2EqSd.js → railroadDiagram-RFXS5EU6-xgLekU3B.js} +1 -1
- package/dist/assets/react-CPH0zySO.js +8 -0
- package/dist/assets/{refresh-cw-DZ_Y89ld.js → refresh-cw-vTYZqC0b.js} +1 -1
- package/dist/assets/remote-DgfRmokf.js +1 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-BiXNX1NN.js → requirementDiagram-TGXJPOKE-CyVUzEX5.js} +1 -1
- package/dist/assets/rotate-cw-L_seGUwq.js +1 -0
- package/dist/assets/runtime-config-page-V75_zqXa.js +1 -0
- package/dist/assets/{sankeyDiagram-HTMAVEWB-qzDeLztW.js → sankeyDiagram-HTMAVEWB-JK6bFmYB.js} +1 -1
- package/dist/assets/{save-BgMfxs5u.js → save-lWb6TI69.js} +1 -1
- package/dist/assets/{search-C7ruoV7u.js → search-BuUX_0KY.js} +1 -1
- package/dist/assets/{search-config-page-BTjfOZ6q.js → search-config-page-rlkRJZCF.js} +1 -1
- package/dist/assets/{secrets-config-page-BFjiSz73.js → secrets-config-page-BxCEc8r5.js} +2 -2
- package/dist/assets/security-config-Cqxe13za.js +1 -0
- package/dist/assets/{select-BMX2ooE-.js → select-oalsltZV.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BtRbPQtL.js → sequenceDiagram-DBY2YBRQ-BR5IokXV.js} +1 -1
- package/dist/assets/{settings-2-BNQF6hed.js → settings-2-Bd9xiNAY.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-CMYvM3SL.js → sizeCapture-X5ZJPWSS-SxG_YnMA.js} +1 -1
- package/dist/assets/skeleton-D72EdBNa.js +1 -0
- package/dist/assets/{src-Bw-mzbQb.js → src-vgz8E_-O.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BJ-mFeOz.js → stateDiagram-2N3HPSRC-DD4OoYhn.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-ZC6dFORv.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BsucVOO2.js → swimlanes-5IMT3BWC-BWkFeyCT.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-CRkh51Im.js +8 -0
- package/dist/assets/{tag-chip-BL-UXlVO.js → tag-chip-B324d_Xk.js} +1 -1
- package/dist/assets/{timeline-definition-FHXFAJF6-Bicewlzo.js → timeline-definition-FHXFAJF6-VTRPy6KA.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-Bw0DSCH7.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-DFWBFcfh.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-BxOOnO9x.js → vennDiagram-L72KCM5P-Bvk42Pek.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CoWturk0.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-Cl7NltMv.js → wardleyDiagram-EHGQE667-DOFjx2s3.js} +1 -1
- package/dist/assets/x-Cps-je1K.js +1 -0
- package/dist/assets/{xychartDiagram-FW5EYKEG-DiMIxQ7N.js → xychartDiagram-FW5EYKEG-DY5yRTUc.js} +1 -1
- package/dist/index.html +22 -21
- package/package.json +9 -9
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +46 -0
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +2 -0
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +52 -2
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +97 -10
- package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +4 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +10 -0
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +27 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-child-sessions.test.tsx +51 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx +90 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +9 -17
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +4 -45
- package/src/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions.tsx +66 -0
- package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +171 -0
- package/src/features/chat/types/chat-message.types.ts +5 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser-tab-strip.test.tsx +78 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +71 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +78 -1
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +97 -44
- package/src/shared/lib/api/__tests__/ncp-session-query-cache.utils.test.ts +2 -0
- package/src/shared/lib/api/ncp-session-query-cache.utils.ts +4 -0
- package/src/shared/lib/api/ncp-session.types.ts +3 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +8 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
- package/dist/assets/architecture-TIHT7OUA-DO2x53FK.js +0 -1
- package/dist/assets/channel-nBvMaVyP.js +0 -1
- package/dist/assets/chunk-2Q5K7J3B-CJgqFDS4.js +0 -1
- package/dist/assets/chunk-VAUOI2AC-BGvh3yIx.js +0 -1
- package/dist/assets/chunk-XXDRQBXY-DaBxG5gs.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-P6N03G2R.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B1_uLvMv.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-B-H8Qp-n.js +0 -1
- package/dist/assets/doc-browser-BbirIYbj.js +0 -1
- package/dist/assets/doc-browser-Crqog5cn.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-YVtShyFA.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-BeKXabVO.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-jTSiBj-p.js +0 -1
- package/dist/assets/index-1f4Pv2q3.css +0 -1
- package/dist/assets/index-BYWLjYFQ.js +0 -109
- package/dist/assets/info-DKCQHKI2-BIporNWb.js +0 -1
- package/dist/assets/loader-circle-LVHz84Xj.js +0 -1
- package/dist/assets/mcp-marketplace-page-2SsrbaiQ.js +0 -1
- package/dist/assets/mcp-marketplace-page-XgYLOAkK.js +0 -9
- package/dist/assets/model-config-page-DL8ljJK-.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CuFMADV0.js +0 -1
- package/dist/assets/pie-RZYD4A2V-XUeIXhM9.js +0 -1
- package/dist/assets/plus-BeGZHC3f.js +0 -1
- package/dist/assets/radar-I7S5WNFK-D-NNGh1k.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-C67_uDaM.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-BZ8Yflnd.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-B7zO7RoK.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6-R63PTgLz.js +0 -1
- package/dist/assets/react-DND3vEAM.js +0 -8
- package/dist/assets/remote-Codkr9SX.js +0 -1
- package/dist/assets/rotate-cw-CP3pQ4R8.js +0 -1
- package/dist/assets/runtime-config-page-Um1XDqXJ.js +0 -1
- package/dist/assets/security-config-Dx-wMb4q.js +0 -1
- package/dist/assets/skeleton-CWZ0IO2D.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BE-HqxOK.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dr1H9LJH.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-npJ5dbqS.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-pYrBx6oa.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CmSRKLR8.js +0 -1
- package/dist/assets/x-BSu1xwPK.js +0 -1
- /package/dist/assets/{array-BQVzeGRt.js → array-NADKd9aU.js} +0 -0
- /package/dist/assets/{chunk-KEIR6QF5-f9psyCXK.js → chunk-KEIR6QF5-BuyuzJJL.js} +0 -0
- /package/dist/assets/{chunk-Y2CYZVJY-D1mAU_Po.js → chunk-Y2CYZVJY-D_B2ZRNt.js} +0 -0
- /package/dist/assets/{config-hints-nV5HPOvq.js → config-hints-BcfVUMij.js} +0 -0
- /package/dist/assets/{cytoscape.esm-0uDtdD0l.js → cytoscape.esm-BANTlgza.js} +0 -0
- /package/dist/assets/{defaultLocale-DSWXGgt0.js → defaultLocale-C4_L0aEs.js} +0 -0
- /package/dist/assets/{dist--ta5rX12.js → dist-DAPEIJbz.js} +0 -0
- /package/dist/assets/{graphlib-CQ6QV4yP.js → graphlib-ChqXg0dI.js} +0 -0
- /package/dist/assets/{host-capabilities-Tk038i04.js → host-capabilities-D9C4CBK5.js} +0 -0
- /package/dist/assets/{init-Y6h9jGtZ.js → init-D8wZg9xq.js} +0 -0
- /package/dist/assets/{jszip.min-8hyJpgpx.js → jszip.min-CkbBfwAQ.js} +0 -0
- /package/dist/assets/{katex-ARhY9PS1.js → katex-TZJFa0J_.js} +0 -0
- /package/dist/assets/{middleware-DXsbf9xI.js → middleware-B3k-TSmZ.js} +0 -0
- /package/dist/assets/{path-10BjdyLh.js → path-DodfxcX7.js} +0 -0
- /package/dist/assets/{rough.esm-DxgC_14r.js → rough.esm-CUaa1Rbe.js} +0 -0
- /package/dist/assets/{xlsx-DrDuDNAa.js → xlsx-CxByi7bR.js} +0 -0
|
@@ -85,7 +85,10 @@ describe("useHydratedNcpAgent", () => {
|
|
|
85
85
|
await waitFor(() => {
|
|
86
86
|
expect(result.current.isHydrating).toBe(false);
|
|
87
87
|
});
|
|
88
|
-
expect(mocks.stream).toHaveBeenCalledWith(
|
|
88
|
+
expect(mocks.stream).toHaveBeenCalledWith(
|
|
89
|
+
{ sessionId: "session-a" },
|
|
90
|
+
expect.objectContaining({ onOpen: expect.any(Function) }),
|
|
91
|
+
);
|
|
89
92
|
|
|
90
93
|
rerender({ sessionId: "session-b" });
|
|
91
94
|
|
|
@@ -94,7 +97,10 @@ describe("useHydratedNcpAgent", () => {
|
|
|
94
97
|
await waitFor(() => {
|
|
95
98
|
expect(result.current.isHydrating).toBe(false);
|
|
96
99
|
});
|
|
97
|
-
expect(mocks.stream).toHaveBeenCalledWith(
|
|
100
|
+
expect(mocks.stream).toHaveBeenCalledWith(
|
|
101
|
+
{ sessionId: "session-b" },
|
|
102
|
+
expect.objectContaining({ onOpen: expect.any(Function) }),
|
|
103
|
+
);
|
|
98
104
|
expect(mocks.stream).toHaveBeenCalledTimes(2);
|
|
99
105
|
});
|
|
100
106
|
|
|
@@ -207,12 +213,13 @@ describe("useHydratedNcpAgent", () => {
|
|
|
207
213
|
expect(result.current.isHydrating).toBe(false);
|
|
208
214
|
});
|
|
209
215
|
expect(loadSeed).not.toHaveBeenCalled();
|
|
210
|
-
expect(mocks.stream).toHaveBeenCalledWith(
|
|
211
|
-
sessionId: "session-materialized",
|
|
212
|
-
|
|
216
|
+
expect(mocks.stream).toHaveBeenCalledWith(
|
|
217
|
+
{ sessionId: "session-materialized" },
|
|
218
|
+
expect.objectContaining({ onOpen: expect.any(Function) }),
|
|
219
|
+
);
|
|
213
220
|
});
|
|
214
221
|
|
|
215
|
-
it("reloads missed state and
|
|
222
|
+
it("reloads missed state and remains usable after repeated live stream disconnects", async () => {
|
|
216
223
|
vi.useFakeTimers();
|
|
217
224
|
try {
|
|
218
225
|
const connected = new Promise<void>(() => {});
|
|
@@ -220,9 +227,15 @@ describe("useHydratedNcpAgent", () => {
|
|
|
220
227
|
stop: mocks.stop.mockResolvedValue(undefined),
|
|
221
228
|
stream: mocks.stream
|
|
222
229
|
.mockRejectedValueOnce(new Error("stream disconnected"))
|
|
230
|
+
.mockRejectedValueOnce(new Error("stream disconnected again"))
|
|
223
231
|
.mockImplementationOnce(() => connected),
|
|
224
232
|
subscribe: vi.fn(() => () => {}),
|
|
225
|
-
send: mocks.send
|
|
233
|
+
send: mocks.send.mockResolvedValue({
|
|
234
|
+
sessionId: "session-recovery",
|
|
235
|
+
userMessageId: "user-after-recovery",
|
|
236
|
+
assistantMessageId: null,
|
|
237
|
+
runId: null,
|
|
238
|
+
}),
|
|
226
239
|
} as unknown as NcpAgentClientEndpoint;
|
|
227
240
|
const recoveredMessage = {
|
|
228
241
|
id: "message-recovered",
|
|
@@ -235,6 +248,7 @@ describe("useHydratedNcpAgent", () => {
|
|
|
235
248
|
const loadSeed = vi
|
|
236
249
|
.fn()
|
|
237
250
|
.mockResolvedValueOnce({ messages: [], status: "running" })
|
|
251
|
+
.mockResolvedValueOnce({ messages: [], status: "running" })
|
|
238
252
|
.mockResolvedValueOnce({
|
|
239
253
|
messages: [recoveredMessage],
|
|
240
254
|
status: "idle",
|
|
@@ -254,15 +268,22 @@ describe("useHydratedNcpAgent", () => {
|
|
|
254
268
|
await act(async () => {
|
|
255
269
|
await vi.runOnlyPendingTimersAsync();
|
|
256
270
|
});
|
|
271
|
+
await vi.waitFor(() => expect(mocks.stream).toHaveBeenCalledTimes(2));
|
|
272
|
+
await act(async () => {
|
|
273
|
+
await vi.runOnlyPendingTimersAsync();
|
|
274
|
+
});
|
|
257
275
|
await vi.waitFor(() => {
|
|
258
276
|
expect(result.current.visibleMessages).toEqual([recoveredMessage]);
|
|
259
277
|
});
|
|
260
278
|
|
|
261
|
-
expect(loadSeed).toHaveBeenCalledTimes(
|
|
262
|
-
expect(mocks.stream).toHaveBeenCalledTimes(
|
|
263
|
-
expect(mocks.send).not.toHaveBeenCalled();
|
|
279
|
+
expect(loadSeed).toHaveBeenCalledTimes(3);
|
|
280
|
+
expect(mocks.stream).toHaveBeenCalledTimes(3);
|
|
264
281
|
expect(result.current.hydrateError).toBeNull();
|
|
265
282
|
expect(result.current.isRunning).toBe(false);
|
|
283
|
+
await act(async () => {
|
|
284
|
+
await result.current.send("after recovery");
|
|
285
|
+
});
|
|
286
|
+
expect(mocks.send).toHaveBeenCalledOnce();
|
|
266
287
|
} finally {
|
|
267
288
|
vi.useRealTimers();
|
|
268
289
|
}
|
|
@@ -306,3 +327,69 @@ describe("useHydratedNcpAgent", () => {
|
|
|
306
327
|
}
|
|
307
328
|
});
|
|
308
329
|
});
|
|
330
|
+
|
|
331
|
+
describe("useHydratedNcpAgent stream reconciliation", () => {
|
|
332
|
+
it("reconciles history after the stream opens without overwriting newer live completion", async () => {
|
|
333
|
+
let subscriber: NcpEndpointSubscriber | null = null;
|
|
334
|
+
let resolveReconcile: ((seed: { messages: readonly NcpMessage[]; status: "running" }) => void) | null = null;
|
|
335
|
+
const reconcileSeed = new Promise<{ messages: readonly NcpMessage[]; status: "running" }>((resolve) => {
|
|
336
|
+
resolveReconcile = resolve;
|
|
337
|
+
});
|
|
338
|
+
const client = {
|
|
339
|
+
stop: mocks.stop.mockResolvedValue(undefined),
|
|
340
|
+
stream: mocks.stream.mockImplementation((_payload, observer) => {
|
|
341
|
+
observer?.onOpen?.();
|
|
342
|
+
return new Promise<void>(() => {});
|
|
343
|
+
}),
|
|
344
|
+
subscribe: vi.fn((nextSubscriber: NcpEndpointSubscriber) => {
|
|
345
|
+
subscriber = nextSubscriber;
|
|
346
|
+
return () => {};
|
|
347
|
+
}),
|
|
348
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
349
|
+
const loadSeed = vi
|
|
350
|
+
.fn()
|
|
351
|
+
.mockResolvedValueOnce({ messages: [], status: "running" })
|
|
352
|
+
.mockImplementationOnce(() => reconcileSeed);
|
|
353
|
+
const finalMessage: NcpMessage = {
|
|
354
|
+
id: "assistant-gap",
|
|
355
|
+
sessionId: "session-gap",
|
|
356
|
+
role: "assistant",
|
|
357
|
+
status: "final",
|
|
358
|
+
parts: [{ type: "text", text: "Recovered without refresh" }],
|
|
359
|
+
timestamp: "2026-08-14T00:00:00.000Z",
|
|
360
|
+
};
|
|
361
|
+
const { result } = renderHook(() =>
|
|
362
|
+
useHydratedNcpAgent({
|
|
363
|
+
sessionId: "session-gap",
|
|
364
|
+
client,
|
|
365
|
+
loadSeed,
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
await waitFor(() => expect(loadSeed).toHaveBeenCalledTimes(2));
|
|
370
|
+
act(() => {
|
|
371
|
+
subscriber?.({
|
|
372
|
+
type: NcpEventType.MessageSent,
|
|
373
|
+
payload: { sessionId: "session-gap", message: finalMessage },
|
|
374
|
+
});
|
|
375
|
+
subscriber?.({
|
|
376
|
+
type: NcpEventType.RunFinished,
|
|
377
|
+
payload: { sessionId: "session-gap", runId: "run-gap" },
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
await waitFor(() => expect(result.current.visibleMessages).toEqual([finalMessage]));
|
|
381
|
+
|
|
382
|
+
await act(async () => {
|
|
383
|
+
resolveReconcile?.({
|
|
384
|
+
messages: [{ ...finalMessage, status: "streaming", parts: [{ type: "text", text: "Recovered" }] }],
|
|
385
|
+
status: "running",
|
|
386
|
+
});
|
|
387
|
+
await Promise.resolve();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(result.current.visibleMessages).toEqual([finalMessage]);
|
|
392
|
+
expect(result.current.isRunning).toBe(false);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -19,6 +19,8 @@ import { nextclawClient } from "@/shared/lib/api";
|
|
|
19
19
|
|
|
20
20
|
const NCP_AGENT_UNAVAILABLE_DURING_STARTUP =
|
|
21
21
|
"ncp agent unavailable during startup";
|
|
22
|
+
const NCP_AGENT_STREAM_OPEN_TIMEOUT_MS = 15_000;
|
|
23
|
+
const NCP_AGENT_STREAM_IDLE_TIMEOUT_MS = 70_000;
|
|
22
24
|
|
|
23
25
|
export { fetchNcpSessionConversationSeed } from "@/features/chat/features/ncp/hooks/use-ncp-session-message-history";
|
|
24
26
|
|
|
@@ -42,6 +44,8 @@ export function createNcpSessionConversationClient(): NcpHttpAgentClientEndpoint
|
|
|
42
44
|
baseUrl: API_BASE,
|
|
43
45
|
basePath: "/api/ncp/agent",
|
|
44
46
|
fetchImpl: createNcpAppClientFetch(),
|
|
47
|
+
streamOpenTimeoutMs: NCP_AGENT_STREAM_OPEN_TIMEOUT_MS,
|
|
48
|
+
streamIdleTimeoutMs: NCP_AGENT_STREAM_IDLE_TIMEOUT_MS,
|
|
45
49
|
});
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
2
|
import {
|
|
3
3
|
fetchNcpSessionSkills,
|
|
4
|
+
fetchNcpSessionTokenUsage,
|
|
4
5
|
fetchNcpSessions
|
|
5
6
|
} from '@/shared/lib/api';
|
|
6
7
|
|
|
@@ -30,3 +31,12 @@ export function useNcpSessionSkills(params: {
|
|
|
30
31
|
...ncpSessionQueryDefaults
|
|
31
32
|
});
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
export function useNcpSessionTokenUsage(sessionId: string | null) {
|
|
36
|
+
return useQuery({
|
|
37
|
+
queryKey: ['ncp-session-token-usage', sessionId],
|
|
38
|
+
queryFn: () => fetchNcpSessionTokenUsage(sessionId as string),
|
|
39
|
+
enabled: Boolean(sessionId),
|
|
40
|
+
...ncpSessionQueryDefaults
|
|
41
|
+
});
|
|
42
|
+
}
|
package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx
ADDED
|
@@ -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(
|
|
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
|
package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx
CHANGED
|
@@ -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
|
-
|
|
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 <
|
|
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
|
+
}
|