@nextclaw/ui 0.15.3 → 0.15.5

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 (314) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/assets/abnfDiagram-VRR7QNED-BYpK0F5F.js +1 -0
  3. package/dist/assets/aiden0z-pptx-renderer.es-D87ywFdC.js +94 -0
  4. package/dist/assets/api-IlX_CbwU.js +13 -0
  5. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +106 -0
  6. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  7. package/dist/assets/appearance-settings-page-80fLa4-9.js +1 -0
  8. package/dist/assets/arc-DFELHWM2.js +1 -0
  9. package/dist/assets/architecture-TIHT7OUA-ClwVFq-K.js +1 -0
  10. package/dist/assets/architectureDiagram-ZJ3FMSHR-CvRhdovL.js +36 -0
  11. package/dist/assets/array-BqYdCIE6.js +1 -0
  12. package/dist/assets/blockDiagram-677ZJIJ3-DVYNkbR_.js +132 -0
  13. package/dist/assets/{book-open-C3mJGwiy.js → book-open-CN7Shpij.js} +1 -1
  14. package/dist/assets/c4Diagram-LMCZKHZV-B1w-iwRe.js +10 -0
  15. package/dist/assets/channel-BEHOSFc5.js +1 -0
  16. package/dist/assets/channels-list-page-R7M6pzp6.js +8 -0
  17. package/dist/assets/chat-page-152YuwfU.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-DrIhesSX.js +1 -0
  19. package/dist/assets/chunk-32BRIVSS-kG3lg0bC.js +1 -0
  20. package/dist/assets/chunk-52WLFC77-BLeB7iGk.js +10 -0
  21. package/dist/assets/chunk-5FCAYU7R-BQga1Rr7.js +1 -0
  22. package/dist/assets/chunk-5HE753X5-DU81hkcF.js +1 -0
  23. package/dist/assets/chunk-5JV3BV7I-C2bAx0m7.js +1 -0
  24. package/dist/assets/chunk-5TONJI2A-BExIIgWY.js +2 -0
  25. package/dist/assets/chunk-5VM5RSS4-C3XB05yh.js +15 -0
  26. package/dist/assets/chunk-7BUUIJ7U-JiyRtrGu.js +1 -0
  27. package/dist/assets/chunk-BIQX33UG-BZRV20pI.js +1 -0
  28. package/dist/assets/chunk-C7G6YPKG-AsyDSQ9j.js +1 -0
  29. package/dist/assets/chunk-CQNSW5MT-iN-fiXVA.js +1 -0
  30. package/dist/assets/chunk-CYSBUYHQ-DhC_TRzI.js +1 -0
  31. package/dist/assets/chunk-DseTPa7n.js +1 -0
  32. package/dist/assets/chunk-EMLP6XTP-QFLK8R6d.js +1 -0
  33. package/dist/assets/chunk-EX3LRPZG-DzUwhkCz.js +231 -0
  34. package/dist/assets/chunk-FWX5IMBZ-DFelpZBy.js +2 -0
  35. package/dist/assets/chunk-HOUHSVGY-BMPwFEwN.js +1 -0
  36. package/dist/assets/chunk-ICXQ74PX-DH8NJMok.js +2 -0
  37. package/dist/assets/chunk-JG7HCLWE-D-95AsO4.js +2 -0
  38. package/dist/assets/chunk-JWPE2WC7-DIaTv8kx.js +1 -0
  39. package/dist/assets/chunk-KEIR6QF5-A3jEqwzn.js +161 -0
  40. package/dist/assets/chunk-MOJQB5TN-BPFLzWgj.js +88 -0
  41. package/dist/assets/chunk-MOZMSUNE-DR_fzxG0.js +1 -0
  42. package/dist/assets/chunk-OGEWGWER-TIcEoPPH.js +1 -0
  43. package/dist/assets/chunk-OSBZ3O6U-BALNvSfG.js +1 -0
  44. package/dist/assets/chunk-PUDLZKDR-I-EXhf_k.js +156 -0
  45. package/dist/assets/chunk-Q4XR5HBZ-uvIGstBS.js +70 -0
  46. package/dist/assets/chunk-QBLGF6JB-r-TjLdxa.js +1 -0
  47. package/dist/assets/chunk-R7FJI6CG-N-1LDgvy.js +1 -0
  48. package/dist/assets/chunk-RYQCIY6F-jxNASx65.js +1 -0
  49. package/dist/assets/chunk-U6XO7XAA-Cphbuc7q.js +2 -0
  50. package/dist/assets/chunk-V7JOEXUC-BLEuBf1s.js +206 -0
  51. package/dist/assets/chunk-VAUOI2AC-Dpcz6BhT.js +1 -0
  52. package/dist/assets/chunk-VR4S4FIN-BAUbR0ke.js +1 -0
  53. package/dist/assets/chunk-WYO6CB5R-Do6SJrnY.js +127 -0
  54. package/dist/assets/chunk-XXDRQBXY-C2QjjwYX.js +1 -0
  55. package/dist/assets/chunk-Y2CYZVJY-BGrd8tLy.js +1 -0
  56. package/dist/assets/chunk-YOTPTUD7-CQqvMTCr.js +1 -0
  57. package/dist/assets/chunk-ZGVPDNZ5-B7kWWaXv.js +62 -0
  58. package/dist/assets/chunk-ZIRB5QZD-DTU0cDNh.js +32 -0
  59. package/dist/assets/classDiagram-OUVF2IWQ-DESMaY3_.js +1 -0
  60. package/dist/assets/classDiagram-v2-EOCWNBFH-DwOlByYH.js +1 -0
  61. package/dist/assets/config-split-page-BlfvfmTm.js +1 -0
  62. package/dist/assets/confirm-dialog-Dt8Kdbgp.js +5 -0
  63. package/dist/assets/cose-bilkent-JH36ORCC-B9LrtV-q.js +1 -0
  64. package/dist/assets/cynefin-VYW2F7L2-CzjeeOq9.js +1 -0
  65. package/dist/assets/cynefinDiagram-TSTJHNR4-BfjvW4su.js +62 -0
  66. package/dist/assets/cytoscape.esm-CRW8q92n.js +321 -0
  67. package/dist/assets/dagre-DkiXZKkL.js +1 -0
  68. package/dist/assets/dagre-VKFMJZFB-B1pQKQSY.js +4 -0
  69. package/dist/assets/defaultLocale-Cdv9x6q7.js +1 -0
  70. package/dist/assets/desktop-update-config-DreMJu9z.js +1 -0
  71. package/dist/assets/diagram-FQU43EPY-cB4EZdaP.js +3 -0
  72. package/dist/assets/diagram-G47NLZAW-CtW8im1B.js +24 -0
  73. package/dist/assets/diagram-NH7WQ7WH-BDCtrCCm.js +24 -0
  74. package/dist/assets/diagram-OA4YK3LP-DEMJ42Gt.js +30 -0
  75. package/dist/assets/diagram-WEI45ONY-CdCcxSYU.js +41 -0
  76. package/dist/assets/dist-D5ycbwCo.js +41 -0
  77. package/dist/assets/dist-dw-gAfM_.js +1 -0
  78. package/dist/assets/doc-browser-C9u7N9vH.js +1 -0
  79. package/dist/assets/doc-browser-DJQ5XPsi2.js +1 -0
  80. package/dist/assets/doc-browser-context-CCdW9TJf.js +1 -0
  81. package/dist/assets/docx-preview-zis3U0Fa.js +20 -0
  82. package/dist/assets/download-CyPgglKr.js +1 -0
  83. package/dist/assets/ebnfDiagram-CCIWWBDH-DBZETBwE.js +1 -0
  84. package/dist/assets/erDiagram-Q63AITRT-y2zDXprP.js +85 -0
  85. package/dist/assets/eventmodeling-45OFAUF4-C38BqG8w.js +1 -0
  86. package/dist/assets/{external-link-CU0kzzIP.js → external-link-BahW1sl3.js} +1 -1
  87. package/dist/assets/eye-D8gGXMkD.js +1 -0
  88. package/dist/assets/flowDiagram-23GEKE2U-BoKk8RRq.js +1 -0
  89. package/dist/assets/folder-DGWqFDvE.js +1 -0
  90. package/dist/assets/{form-actions-BvCp_0lS.js → form-actions-D2imZLnU.js} +1 -1
  91. package/dist/assets/ganttDiagram-NO4QXBWP-D2DP344e.js +292 -0
  92. package/dist/assets/gitGraph-TEB2WS4Q-IsQHHJuS.js +1 -0
  93. package/dist/assets/gitGraphDiagram-IHSO6WYX-D37uiwA2.js +106 -0
  94. package/dist/assets/graphlib-BcNFQr5y.js +1 -0
  95. package/dist/assets/index-0av6WO9-.css +1 -0
  96. package/dist/assets/index-CT0iYlxV.js +2 -0
  97. package/dist/assets/info-DKCQHKI2-ofy2qlHI.js +1 -0
  98. package/dist/assets/infoDiagram-FWYZ7A6U-CG-WAIRi.js +2 -0
  99. package/dist/assets/init-D8TjIqfm.js +1 -0
  100. package/dist/assets/ishikawaDiagram-FXEZZL3T-WeCTnW0f.js +70 -0
  101. package/dist/assets/journeyDiagram-5HDEW3XC-DUFBW9Td.js +139 -0
  102. package/dist/assets/jszip.min-CP5uPNQa.js +2 -0
  103. package/dist/assets/kanban-definition-HUTT4EX6-B0lxgMpg.js +89 -0
  104. package/dist/assets/katex-wEmrEFTg.js +257 -0
  105. package/dist/assets/line-B78frP_c.js +1 -0
  106. package/dist/assets/linear-C6lVEcb2.js +1 -0
  107. package/dist/assets/loader-circle-DtKek-qG.js +1 -0
  108. package/dist/assets/logo-badge-CuPetu-v.js +1 -0
  109. package/dist/assets/map-Bdri5UL5.js +1 -0
  110. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +1 -0
  111. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +9 -0
  112. package/dist/assets/mermaid-parser.core-Cue18vLo.js +4 -0
  113. package/dist/assets/mermaid.core-CXwWRh5i.js +11 -0
  114. package/dist/assets/mindmap-definition-LN4V7U3C-CFxz0P0-.js +96 -0
  115. package/dist/assets/model-config-page-DiJcvBML.js +1 -0
  116. package/dist/assets/ordinal-Cbd8t4LB.js +1 -0
  117. package/dist/assets/packet-7NZHBO7P-Rfopfa1o.js +1 -0
  118. package/dist/assets/path-DzBnMI1r.js +1 -0
  119. package/dist/assets/pegDiagram-2B236MQR-B_Qt9zyT.js +1 -0
  120. package/dist/assets/pie-RZYD4A2V-CJR1xKEv.js +1 -0
  121. package/dist/assets/pieDiagram-ENE6RG2P-CRQdSuwS.js +39 -0
  122. package/dist/assets/plus-BwgE-Zwf.js +1 -0
  123. package/dist/assets/popover-Btp7KVc3.js +1 -0
  124. package/dist/assets/preload-helper-D4M6sveU.js +1 -0
  125. package/dist/assets/provider-models-BoUhFo9R.js +1 -0
  126. package/dist/assets/provider-scoped-model-input-DZMtoAJG.js +1 -0
  127. package/dist/assets/providers-config-page-DfiWveEF.js +1 -0
  128. package/dist/assets/quadrantDiagram-ABIIQ3AL-D5kPpttq.js +7 -0
  129. package/dist/assets/radar-I7S5WNFK-CDg8Z-1r.js +1 -0
  130. package/dist/assets/railroad-3IZDKUUU-BHnNmjxO.js +1 -0
  131. package/dist/assets/railroad-abnf-AHOZXSZD-DGtNcusW.js +1 -0
  132. package/dist/assets/railroad-ebnf-EBAXGLYW-BRFtsQmm.js +1 -0
  133. package/dist/assets/railroad-peg-LSFZ7HO6-Bj5IypCA.js +1 -0
  134. package/dist/assets/railroadDiagram-RFXS5EU6-DGh_TGlQ.js +1 -0
  135. package/dist/assets/react-BY-U3h3t.js +8 -0
  136. package/dist/assets/{refresh-cw-CILd1vAP.js → refresh-cw-Cme5qFej.js} +1 -1
  137. package/dist/assets/remote-BaVECEZw.js +1 -0
  138. package/dist/assets/requirementDiagram-TGXJPOKE-EmFph0H_.js +84 -0
  139. package/dist/assets/rotate-cw-CMRZxIkH.js +1 -0
  140. package/dist/assets/rough.esm-wJ1d3nQl.js +1 -0
  141. package/dist/assets/runtime-config-page-wygwvMB4.js +1 -0
  142. package/dist/assets/sankeyDiagram-HTMAVEWB-BXWB5Ugi.js +40 -0
  143. package/dist/assets/{save-ByJqcPTP.js → save-DNg2yJTS.js} +1 -1
  144. package/dist/assets/{search-DVIV6pfD.js → search-3ykfrAFt.js} +1 -1
  145. package/dist/assets/search-config-page-DrVKa_Ls.js +1 -0
  146. package/dist/assets/secrets-config-page-B39uQ5kF.js +3 -0
  147. package/dist/assets/security-config-BPzet76z.js +1 -0
  148. package/dist/assets/select-B5EecXJt.js +1 -0
  149. package/dist/assets/sequenceDiagram-DBY2YBRQ-ClKnO7v0.js +162 -0
  150. package/dist/assets/setting-row-Qv1Ps79v.js +1 -0
  151. package/dist/assets/{settings-2-CH5shGg_.js → settings-2-D-3um9Az.js} +1 -1
  152. package/dist/assets/sizeCapture-X5ZJPWSS-DSo-t5ou.js +1 -0
  153. package/dist/assets/{skeleton-DeX1FKw7.js → skeleton-BgkzQd4U.js} +1 -1
  154. package/dist/assets/src-Dad2ZbCt.js +1 -0
  155. package/dist/assets/stateDiagram-2N3HPSRC-BvAalBfz.js +1 -0
  156. package/dist/assets/stateDiagram-v2-6OUMAXLB-CfMU38yc.js +1 -0
  157. package/dist/assets/swimlanes-5IMT3BWC-DdVqVa1a.js +2 -0
  158. package/dist/assets/swimlanesDiagram-G3AALYLV-D9Zb9X57.js +8 -0
  159. package/dist/assets/tabs-BVD8gp3y.js +1 -0
  160. package/dist/assets/tag-chip-0qn-f3Bo.js +1 -0
  161. package/dist/assets/timeline-definition-FHXFAJF6-D4ar8IQY.js +120 -0
  162. package/dist/assets/trash-2-cnsMppRC.js +1 -0
  163. package/dist/assets/treeView-QDETBFTQ-Uy7M0WvH.js +1 -0
  164. package/dist/assets/treemap-6X3UGDF4-BJfGV7KX.js +1 -0
  165. package/dist/assets/use-config-D6yx0lb0.js +1 -0
  166. package/dist/assets/use-confirm-dialog-C_o0eQeH.js +1 -0
  167. package/dist/assets/use-viewport-layout-BWav4KCL.js +1 -0
  168. package/dist/assets/vennDiagram-L72KCM5P-CDXJ_i3p.js +34 -0
  169. package/dist/assets/wardley-OPB4EBWU-q263iF3K.js +1 -0
  170. package/dist/assets/wardleyDiagram-EHGQE667-D-dSuT_7.js +78 -0
  171. package/dist/assets/x-B3o1dWR5.js +1 -0
  172. package/dist/assets/xlsx-DUqJhg5u.js +64 -0
  173. package/dist/assets/xychartDiagram-FW5EYKEG-C9JCzsZF.js +7 -0
  174. package/dist/index.html +35 -28
  175. package/package.json +11 -6
  176. package/src/features/chat/components/chat-popover-content.tsx +1 -0
  177. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  178. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  179. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  180. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  181. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  182. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  183. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  184. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +2 -0
  185. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  186. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +3 -14
  187. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  188. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +97 -65
  189. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +43 -0
  190. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +14 -10
  191. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +51 -0
  192. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +61 -21
  193. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +42 -10
  194. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +80 -0
  195. package/src/features/chat/features/message/components/chat-message-list.container.tsx +66 -56
  196. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  197. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +2 -0
  198. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  199. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +4 -3
  200. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  201. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  202. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  203. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  204. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +3 -3
  205. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -3
  206. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
  207. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +424 -129
  208. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  209. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +3 -1
  210. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  211. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +9 -3
  212. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +13 -22
  213. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +96 -194
  214. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  215. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +61 -7
  216. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +34 -8
  217. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  218. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  219. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  220. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  221. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  222. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  223. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  224. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  225. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +157 -13
  226. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +151 -2
  227. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +17 -0
  228. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +66 -0
  229. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  230. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  231. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +196 -43
  232. package/src/features/chat/index.ts +1 -0
  233. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +101 -0
  234. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +48 -60
  235. package/src/features/chat/managers/chat-session-list.manager.ts +1 -0
  236. package/src/features/chat/managers/chat-thread.manager.ts +137 -162
  237. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  238. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +87 -1
  239. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  240. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  241. package/src/features/chat/stores/chat-thread.store.ts +77 -40
  242. package/src/features/chat/types/chat-input-bar.types.ts +2 -0
  243. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  244. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  245. package/src/index.css +36 -2
  246. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  247. package/src/shared/components/common/__tests__/brand-header.test.tsx +17 -7
  248. package/src/shared/components/common/brand-header.tsx +34 -34
  249. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  250. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  251. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  252. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  253. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  254. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  255. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  256. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  257. package/src/shared/components/file-type-icon.tsx +272 -0
  258. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  259. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  260. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  261. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  262. package/src/shared/hooks/use-server-path-read.ts +10 -5
  263. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  264. package/src/shared/lib/api/types.ts +3 -3
  265. package/src/shared/lib/api/utils/server-path.utils.ts +20 -20
  266. package/src/shared/lib/i18n/locales/en-US/chat.json +25 -1
  267. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  268. package/src/shared/lib/i18n/locales/zh-CN/chat.json +25 -1
  269. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  270. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +1 -4
  271. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +1 -18
  272. package/dist/assets/api-iNBpppzj.js +0 -13
  273. package/dist/assets/appearance-settings-page-DMffQFR3.js +0 -1
  274. package/dist/assets/channels-list-page-CCtp0BRS.js +0 -8
  275. package/dist/assets/chat-page-H5a0dWvO.js +0 -1
  276. package/dist/assets/config-split-page-CrOJgB-Y.js +0 -1
  277. package/dist/assets/confirm-dialog-Chv50USy.js +0 -5
  278. package/dist/assets/desktop-update-config-KzTuXRf_.js +0 -1
  279. package/dist/assets/dist-BunzmC4B.js +0 -1
  280. package/dist/assets/dist-ChmGwvtK.js +0 -1
  281. package/dist/assets/doc-browser-DNRL4Zvd.js +0 -1
  282. package/dist/assets/doc-browser-Dg0pmkOg.js +0 -1
  283. package/dist/assets/doc-browser-context-BHY-0FSD.js +0 -1
  284. package/dist/assets/es2015-DQZ5JWwK.js +0 -41
  285. package/dist/assets/i18n-provider-Cu8XyZb_.js +0 -1
  286. package/dist/assets/index-CzfBdvK1.css +0 -1
  287. package/dist/assets/index-DGqHne1Q.js +0 -104
  288. package/dist/assets/loader-circle-CDSFuzmk.js +0 -1
  289. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +0 -9
  290. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +0 -1
  291. package/dist/assets/model-config-page-bNNo-XyG.js +0 -1
  292. package/dist/assets/plus-D4DsdLN-.js +0 -1
  293. package/dist/assets/provider-scoped-model-input-IlgQCsoT.js +0 -1
  294. package/dist/assets/providers-config-page-wLo6CAJw.js +0 -1
  295. package/dist/assets/react-BPIsQIlO.js +0 -8
  296. package/dist/assets/remote-uKEYp7AR.js +0 -1
  297. package/dist/assets/rotate-cw-wPFPg1kH.js +0 -1
  298. package/dist/assets/runtime-config-page-DDn1sP4G.js +0 -1
  299. package/dist/assets/search-config-page-B-Cf4iiB.js +0 -1
  300. package/dist/assets/secrets-config-page-DPKsY8Iw.js +0 -3
  301. package/dist/assets/security-config-e9SSBlKr.js +0 -1
  302. package/dist/assets/select-BdgczSNw.js +0 -1
  303. package/dist/assets/setting-row-BkTVUTKx.js +0 -1
  304. package/dist/assets/side-dock-BsBvcJ9i.js +0 -3
  305. package/dist/assets/switch-Cv8Ekb3x.js +0 -1
  306. package/dist/assets/tag-chip-RB9uKcoC.js +0 -1
  307. package/dist/assets/tooltip-oQlEN-l6.js +0 -1
  308. package/dist/assets/use-config-l3W7y8Qz.js +0 -1
  309. package/dist/assets/use-confirm-dialog-BAxaVOJI.js +0 -1
  310. package/dist/assets/x-BBMffEN0.js +0 -1
  311. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +0 -55
  312. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-LxG80Uj5.js} +0 -0
  313. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-CFbAfrrA.js} +0 -0
  314. /package/dist/assets/{middleware-BILWbJE4.js → middleware-CAXxl-af.js} +0 -0
@@ -1,9 +1,9 @@
1
1
  import { NavLink } from "react-router-dom";
2
2
  import {
3
3
  Popover,
4
- PopoverContent,
5
4
  PopoverTrigger,
6
5
  } from "@/shared/components/ui/popover";
6
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
7
7
  import {
8
8
  Select,
9
9
  SelectContent,
@@ -52,12 +52,6 @@ type ChatSidebarUtilityMenuProps = {
52
52
  collapsed?: boolean;
53
53
  };
54
54
 
55
- function isChatComposerFocusTarget(target: EventTarget | null): boolean {
56
- return target instanceof HTMLElement && Boolean(
57
- target.closest('.nextclaw-chat-input-bar-shell [role="textbox"][contenteditable="true"]'),
58
- );
59
- }
60
-
61
55
  export function ChatSidebarUtilityMenu({
62
56
  isOpen,
63
57
  onOpenChange,
@@ -118,15 +112,10 @@ export function ChatSidebarUtilityMenu({
118
112
  </button>
119
113
  </PopoverTrigger>
120
114
  )}
121
- <PopoverContent
115
+ <ChatPopoverContent
122
116
  side={collapsed ? "right" : "top"}
123
117
  align={collapsed ? "end" : "start"}
124
118
  className="w-64 p-2"
125
- onFocusOutside={(event) => {
126
- if (isChatComposerFocusTarget(event.detail.originalEvent.target)) {
127
- event.preventDefault();
128
- }
129
- }}
130
119
  >
131
120
  <div className="space-y-1">
132
121
  <NavLink
@@ -177,7 +166,7 @@ export function ChatSidebarUtilityMenu({
177
166
  onSelect={onSelectLanguage}
178
167
  onCloseMenu={() => onOpenChange(false)}
179
168
  />
180
- </PopoverContent>
169
+ </ChatPopoverContent>
181
170
  </Popover>
182
171
  );
183
172
  }
@@ -6,7 +6,7 @@ import {
6
6
  type MutableRefObject,
7
7
  } from 'react';
8
8
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
9
- import { describe, expect, it, vi } from 'vitest';
9
+ import { afterEach, describe, expect, it, vi } from 'vitest';
10
10
  import {
11
11
  createChatComposerTextNode,
12
12
  type ChatComposerNode,
@@ -26,9 +26,14 @@ import type {
26
26
  SessionConversationInputSnapshot,
27
27
  } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
28
28
  import { useSessionConversationInputState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
29
+ import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
29
30
 
30
31
  const uploadNcpAssetsMock = vi.hoisted(() => vi.fn());
31
32
 
33
+ afterEach(() => {
34
+ useChatMessageLayoutStore.getState().setLayout('card');
35
+ });
36
+
32
37
  vi.mock('@/app/hooks/use-viewport-layout', () => ({
33
38
  useViewportLayout: () => ({ isDesktop: true, isMobile: false }),
34
39
  }));
@@ -190,6 +195,20 @@ function StreamingSessionConversationInputHarness({
190
195
  }
191
196
 
192
197
  describe('SessionConversationInput streaming stability', () => {
198
+ it('aligns the default input with the flat message reading track', () => {
199
+ useChatMessageLayoutStore.getState().setLayout('flat');
200
+ const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
201
+
202
+ render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
203
+
204
+ const track = document.querySelector(
205
+ '[data-chat-conversation-track="flat"][data-chat-conversation-track-width="composer"]',
206
+ );
207
+ expect(track?.className).toContain('max-w-[min(54rem,100%)]');
208
+ expect(track?.querySelector('.nextclaw-chat-input-bar-shell')).toBeTruthy();
209
+ expect(track?.firstElementChild?.className).toContain('px-0');
210
+ });
211
+
193
212
  it('keeps IME composition stable while simulated streamed output rerenders the owner', async () => {
194
213
  const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
195
214
  render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
@@ -49,6 +49,8 @@ import {
49
49
  } from '@/features/chat/managers/chat-recent-skills.manager';
50
50
 
51
51
  import { useSessionConversationInputAttachments } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-attachments';
52
+ import { ChatConversationTrack } from '@/features/chat/components/conversation/chat-conversation-track';
53
+ import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
52
54
  import type { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
53
55
  import type {
54
56
  SessionConversationQueuedInput,
@@ -65,6 +67,7 @@ import {
65
67
  import { SessionQueuedInputRows } from './session-queued-input-rows';
66
68
 
67
69
  type SessionConversationInputQuery = ReturnType<typeof useSessionConversationInputQuery>;
70
+ type SkillSource = SessionSkillEntryView['source'];
68
71
 
69
72
  export type SessionConversationInputController = {
70
73
  readonly canStopGeneration: boolean;
@@ -80,15 +83,18 @@ export type SessionConversationInputController = {
80
83
 
81
84
  function toSkillRecords(
82
85
  snapshotRecords: SessionSkillEntryView[],
83
- scopeLabels: Record<SessionSkillEntryView['scope'], string>,
86
+ scopeLabels: Record<SkillSource, string>,
87
+ groupLabels: Record<SkillSource, string>,
84
88
  ): ChatSkillRecord[] {
85
89
  return snapshotRecords.map((record) => ({
86
90
  key: record.ref,
87
91
  label: record.name,
88
- scopeLabel: scopeLabels[record.scope],
92
+ scopeLabel: scopeLabels[record.source],
93
+ groupKey: record.source,
94
+ groupLabel: groupLabels[record.source],
89
95
  description: record.description,
90
96
  descriptionZh: record.descriptionZh,
91
- badgeLabel: scopeLabels[record.scope],
97
+ badgeLabel: scopeLabels[record.source],
92
98
  }));
93
99
  }
94
100
 
@@ -107,12 +113,21 @@ function toModelRecords(snapshotModels: SessionConversationInputQuery['modelOpti
107
113
  }
108
114
 
109
115
  function useSessionConversationInputLabels(language: string) {
110
- const skillScopeLabels = useMemo<Record<'builtin' | 'project' | 'workspace', string>>(() => {
116
+ const { skillGroupLabels, skillScopeLabels } = useMemo(() => {
111
117
  void language;
112
118
  return {
113
- builtin: t('chatSkillScopeBuiltin'),
114
- project: t('chatSkillScopeProject'),
115
- workspace: t('chatSkillScopeWorkspace'),
119
+ skillScopeLabels: {
120
+ builtin: t('chatSkillScopeBuiltin'),
121
+ global: t('chatSkillScopeGlobal'),
122
+ project: t('chatSkillScopeProject'),
123
+ workspace: t('chatSkillScopeWorkspace'),
124
+ } satisfies Record<SkillSource, string>,
125
+ skillGroupLabels: {
126
+ builtin: t('chatSkillGroupBuiltin'),
127
+ global: t('chatSkillGroupGlobal'),
128
+ project: t('chatSkillGroupProject'),
129
+ workspace: t('chatSkillGroupWorkspace'),
130
+ } satisfies Record<SkillSource, string>,
116
131
  };
117
132
  }, [language]);
118
133
  const slashTexts = useMemo(() => {
@@ -126,6 +141,7 @@ function useSessionConversationInputLabels(language: string) {
126
141
  }, [language]);
127
142
  return {
128
143
  skillScopeLabels,
144
+ skillGroupLabels,
129
145
  slashTexts,
130
146
  recentModelsLabel: t('chatPickerRecentModels'),
131
147
  allModelsLabel: t('chatPickerAllModels'),
@@ -142,13 +158,15 @@ function useSessionConversationInputLabels(language: string) {
142
158
  function useSessionConversationInputCollections(params: {
143
159
  modelOptions: SessionConversationInputQuery['modelOptions'];
144
160
  skillRecords: SessionSkillEntryView[];
145
- skillScopeLabels: Record<'builtin' | 'project' | 'workspace', string>;
161
+ skillScopeLabels: Record<SkillSource, string>;
162
+ skillGroupLabels: Record<SkillSource, string>;
146
163
  }) {
164
+ const { modelOptions, skillGroupLabels, skillRecords: sourceSkillRecords, skillScopeLabels } = params;
147
165
  const skillRecords = useMemo(
148
- () => toSkillRecords(params.skillRecords, params.skillScopeLabels),
149
- [params.skillRecords, params.skillScopeLabels],
166
+ () => toSkillRecords(sourceSkillRecords, skillScopeLabels, skillGroupLabels),
167
+ [skillGroupLabels, skillScopeLabels, sourceSkillRecords],
150
168
  );
151
- const modelRecords = useMemo(() => toModelRecords(params.modelOptions), [params.modelOptions]);
169
+ const modelRecords = useMemo(() => toModelRecords(modelOptions), [modelOptions]);
152
170
  return {
153
171
  skillRecords,
154
172
  modelRecords,
@@ -187,6 +205,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
187
205
  } = props;
188
206
  const presenter = usePresenter();
189
207
  const { language } = useI18n();
208
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
190
209
  const { isMobile } = useViewportLayout();
191
210
  const inputBarRef = useRef<ChatInputBarHandle | null>(null);
192
211
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -200,6 +219,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
200
219
  } = useSessionConversationInputCollections({
201
220
  modelOptions: inputQuery.modelOptions,
202
221
  skillRecords: inputQuery.skillRecords,
222
+ skillGroupLabels: labels.skillGroupLabels,
203
223
  skillScopeLabels: labels.skillScopeLabels,
204
224
  });
205
225
  const slashCommands = useMemo(() => {
@@ -389,62 +409,74 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
389
409
  selectedSkills: inputSnapshot.selectedSkills,
390
410
  });
391
411
 
412
+ const useReadingTrack = surface === 'default' && messageLayout === 'flat';
413
+ const inputBar = (
414
+ <ChatInputBar
415
+ ref={inputBarRef}
416
+ surface={useReadingTrack ? 'embedded' : surface}
417
+ sendError={inputSnapshot.sendError}
418
+ sendErrorDetailsLabel={t('chatErrorDetails', language)}
419
+ topSlot={controller.queuedInputs.length > 0
420
+ ? <SessionQueuedInputRows controller={controller} />
421
+ : null}
422
+ composer={{
423
+ nodes: [...inputSnapshot.nodes],
424
+ placeholder: textareaPlaceholder,
425
+ disabled: inputDisabled,
426
+ onNodesChange: handleNodesChange,
427
+ onFilesAdd: handleFilesAdd,
428
+ inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
429
+ onInputSurfaceTriggerChange: setInputSurfaceTrigger,
430
+ }}
431
+ inputSurface={inputSurfaceState.panel ?? undefined}
432
+ hint={buildModelStateHint({
433
+ isModelOptionsLoading,
434
+ isModelOptionsEmpty,
435
+ onGoToProviders: presenter.chatUiManager.goToProviders,
436
+ texts: {
437
+ noModelOptionsLabel: t('chatModelNoOptions'),
438
+ configureProviderLabel: t('chatGoConfigureProvider'),
439
+ },
440
+ })}
441
+ toolbar={{
442
+ selects: [],
443
+ trailingSelects: toolbarSelects,
444
+ accessories: [
445
+ {
446
+ key: 'attach',
447
+ label: t('chatInputAttach'),
448
+ icon: 'paperclip' as const,
449
+ iconOnly: true,
450
+ disabled: !attachmentSupported || inputDisabled,
451
+ onClick: () => fileInputRef.current?.click(),
452
+ },
453
+ ],
454
+ skillPicker,
455
+ actions: {
456
+ isSending: controller.isSending,
457
+ canStopGeneration: controller.canStopGeneration,
458
+ sendDisabled: controller.sendDisabled,
459
+ stopDisabled: controller.stopDisabled,
460
+ stopHint: t('chatStopUnavailable'),
461
+ sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
462
+ stopButtonLabel: t('chatStop'),
463
+ contextWindow,
464
+ onSend: controller.send,
465
+ onStop: controller.stop,
466
+ },
467
+ }}
468
+ />
469
+ );
470
+
392
471
  return (
393
472
  <>
394
- <ChatInputBar
395
- ref={inputBarRef}
396
- surface={surface}
397
- topSlot={controller.queuedInputs.length > 0
398
- ? <SessionQueuedInputRows controller={controller} />
399
- : null}
400
- composer={{
401
- nodes: [...inputSnapshot.nodes],
402
- placeholder: textareaPlaceholder,
403
- disabled: inputDisabled,
404
- onNodesChange: handleNodesChange,
405
- onFilesAdd: handleFilesAdd,
406
- inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
407
- onInputSurfaceTriggerChange: setInputSurfaceTrigger,
408
- }}
409
- inputSurface={inputSurfaceState.panel ?? undefined}
410
- hint={buildModelStateHint({
411
- isModelOptionsLoading,
412
- isModelOptionsEmpty,
413
- onGoToProviders: presenter.chatUiManager.goToProviders,
414
- texts: {
415
- noModelOptionsLabel: t('chatModelNoOptions'),
416
- configureProviderLabel: t('chatGoConfigureProvider'),
417
- },
418
- })}
419
- toolbar={{
420
- selects: [],
421
- trailingSelects: toolbarSelects,
422
- accessories: [
423
- {
424
- key: 'attach',
425
- label: t('chatInputAttach'),
426
- icon: 'paperclip' as const,
427
- iconOnly: true,
428
- disabled: !attachmentSupported || inputDisabled,
429
- onClick: () => fileInputRef.current?.click(),
430
- },
431
- ],
432
- skillPicker,
433
- actions: {
434
- sendError: inputSnapshot.sendError,
435
- isSending: controller.isSending,
436
- canStopGeneration: controller.canStopGeneration,
437
- sendDisabled: controller.sendDisabled,
438
- stopDisabled: controller.stopDisabled,
439
- stopHint: t('chatStopUnavailable'),
440
- sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
441
- stopButtonLabel: t('chatStop'),
442
- contextWindow,
443
- onSend: controller.send,
444
- onStop: controller.stop,
445
- },
446
- }}
447
- />
473
+ {useReadingTrack ? (
474
+ <ChatConversationTrack className="pb-4 pt-2" width="composer">
475
+ {inputBar}
476
+ </ChatConversationTrack>
477
+ ) : (
478
+ inputBar
479
+ )}
448
480
  <input
449
481
  ref={fileInputRef}
450
482
  type="file"
@@ -141,6 +141,49 @@ describe('createSlashCommandInputSurfacePlugin', () => {
141
141
  });
142
142
  });
143
143
 
144
+ it('preserves source groups while the skills filter includes every skill section', () => {
145
+ const state = resolveChatInputSurfaceState({
146
+ data: {
147
+ isPanelAppsLoading: false,
148
+ isSkillsLoading: false,
149
+ panelApps: [],
150
+ recentSkillValues: [],
151
+ skillRecords: [
152
+ {
153
+ key: 'project:review',
154
+ label: 'Review',
155
+ groupKey: 'project',
156
+ groupLabel: 'Project skills',
157
+ },
158
+ {
159
+ key: 'global:browser',
160
+ label: 'Browser',
161
+ groupKey: 'global',
162
+ groupLabel: 'Global skills',
163
+ },
164
+ ],
165
+ },
166
+ plugins: [createPlugin()],
167
+ trigger: {
168
+ key: 'slash',
169
+ marker: '/',
170
+ query: '',
171
+ start: 0,
172
+ end: 1,
173
+ },
174
+ });
175
+
176
+ expect(state.panel?.filterOptions?.find((filter) => filter.key === 'skills')).toEqual({
177
+ key: 'skills',
178
+ label: 'Skills',
179
+ sectionKeys: ['skills:project', 'skills:global'],
180
+ });
181
+ expect(state.panel?.items.filter((item) => item.key.startsWith('skill:'))).toMatchObject([
182
+ { sectionKey: 'skills:project', sectionLabel: 'Project skills' },
183
+ { sectionKey: 'skills:global', sectionLabel: 'Global skills' },
184
+ ]);
185
+ });
186
+
144
187
  it('runs a command item through the panel selection handler', () => {
145
188
  const onSelectCommand = vi.fn();
146
189
  const state = resolveChatInputSurfaceState({
@@ -104,8 +104,10 @@ function buildSlashSkillItems(params: {
104
104
  ).map((item) => ({
105
105
  ...item,
106
106
  hintLabel: skillHintLabel,
107
- sectionKey: SLASH_SKILL_SECTION_KEY,
108
- sectionLabel: skillSectionLabel,
107
+ sectionKey: item.sectionKey
108
+ ? `${SLASH_SKILL_SECTION_KEY}:${item.sectionKey}`
109
+ : SLASH_SKILL_SECTION_KEY,
110
+ sectionLabel: item.sectionLabel || skillSectionLabel,
109
111
  }));
110
112
  }
111
113
 
@@ -165,6 +167,14 @@ export function createSlashCommandInputSurfacePlugin(params: {
165
167
  trigger: CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC,
166
168
  resolvePanel: (context) => {
167
169
  const { data, trigger } = context;
170
+ const skillItems = buildSlashSkillItems({
171
+ data,
172
+ itemTexts: params.itemTexts.skillTexts,
173
+ query: trigger.query,
174
+ skillHintLabel: params.labels.skillHintLabel,
175
+ skillSectionLabel: params.labels.skillSectionLabel,
176
+ });
177
+ const skillSectionKeys = [...new Set(skillItems.flatMap((item) => item.sectionKey ? [item.sectionKey] : []))];
168
178
  return {
169
179
  filterOptions: [
170
180
  { key: 'all', label: params.labels.filterAllLabel },
@@ -176,7 +186,7 @@ export function createSlashCommandInputSurfacePlugin(params: {
176
186
  {
177
187
  key: SLASH_SKILL_SECTION_KEY,
178
188
  label: params.labels.filterSkillsLabel,
179
- sectionKeys: [SLASH_SKILL_SECTION_KEY],
189
+ sectionKeys: skillSectionKeys.length > 0 ? skillSectionKeys : [SLASH_SKILL_SECTION_KEY],
180
190
  },
181
191
  {
182
192
  key: SLASH_PANEL_APP_SECTION_KEY,
@@ -195,13 +205,7 @@ export function createSlashCommandInputSurfacePlugin(params: {
195
205
  commandSubtitle: params.labels.commandSubtitle,
196
206
  },
197
207
  }),
198
- ...buildSlashSkillItems({
199
- data,
200
- itemTexts: params.itemTexts.skillTexts,
201
- query: trigger.query,
202
- skillHintLabel: params.labels.skillHintLabel,
203
- skillSectionLabel: params.labels.skillSectionLabel,
204
- }),
208
+ ...skillItems,
205
209
  ...buildSlashPanelAppItems({
206
210
  data,
207
211
  itemTexts: params.itemTexts.panelAppTexts,
@@ -83,6 +83,25 @@ describe('buildChatSlashItems', () => {
83
83
 
84
84
  expect(items.map((item) => item.value)).toEqual(['weather', 'web-search', 'docs']);
85
85
  });
86
+
87
+ it('keeps skill source groups contiguous in their catalog order', () => {
88
+ const items = buildChatSlashItems(
89
+ [
90
+ createSkillRecord({ key: 'project:zeta', label: 'Zeta', groupKey: 'project', groupLabel: 'Project skills' }),
91
+ createSkillRecord({ key: 'global:alpha', label: 'Alpha', groupKey: 'global', groupLabel: 'Global skills' }),
92
+ createSkillRecord({ key: 'project:alpha', label: 'Alpha', groupKey: 'project', groupLabel: 'Project skills' }),
93
+ ],
94
+ '',
95
+ texts,
96
+ );
97
+
98
+ expect(items.map((item) => item.value)).toEqual(['project:alpha', 'project:zeta', 'global:alpha']);
99
+ expect(items.map((item) => [item.sectionKey, item.sectionLabel])).toEqual([
100
+ ['project', 'Project skills'],
101
+ ['project', 'Project skills'],
102
+ ['global', 'Global skills'],
103
+ ]);
104
+ });
86
105
  });
87
106
 
88
107
  describe('buildSkillPickerModel', () => {
@@ -158,6 +177,38 @@ describe('buildSkillPickerModel', () => {
158
177
  ]);
159
178
  });
160
179
 
180
+ it('groups the skill catalog by source when no recent group is visible', () => {
181
+ const model = buildSkillPickerModel({
182
+ skillRecords: [
183
+ createSkillRecord({ key: 'project:review', label: 'Review', groupKey: 'project', groupLabel: 'Project skills' }),
184
+ createSkillRecord({ key: 'workspace:docs', label: 'Docs', groupKey: 'workspace', groupLabel: 'NextClaw skills' }),
185
+ createSkillRecord({ key: 'global:browser', label: 'Browser', groupKey: 'global', groupLabel: 'Global skills' }),
186
+ createSkillRecord({ key: 'builtin:weather', label: 'Weather', groupKey: 'builtin', groupLabel: 'Built-in skills' })
187
+ ],
188
+ recentSkillValues: [],
189
+ groupedRecentSkillValues: [],
190
+ selectedSkills: [],
191
+ isLoading: false,
192
+ onSelectedKeysChange: vi.fn(),
193
+ texts: {
194
+ title: 'Skills',
195
+ searchPlaceholder: 'Search skills',
196
+ emptyLabel: 'No skills',
197
+ loadingLabel: 'Loading',
198
+ manageLabel: 'Manage',
199
+ recentSkillsLabel: 'Recent',
200
+ allSkillsLabel: 'All skills'
201
+ }
202
+ });
203
+
204
+ expect(model.groups?.map((group) => [group.key, group.label])).toEqual([
205
+ ['project', 'Project skills'],
206
+ ['workspace', 'NextClaw skills'],
207
+ ['global', 'Global skills'],
208
+ ['builtin', 'Built-in skills']
209
+ ]);
210
+ });
211
+
161
212
  it('still reorders recent skills even when grouped labels are omitted', () => {
162
213
  const model = buildSkillPickerModel({
163
214
  skillRecords: [
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ChatSkillPickerOption,
3
+ ChatSkillPickerOptionGroup,
3
4
  ChatSkillPickerProps,
4
5
  ChatSlashItem,
5
6
  } from '@nextclaw/agent-chat-ui';
@@ -139,6 +140,13 @@ export function buildChatSlashItems(
139
140
  ): ChatSlashItem[] {
140
141
  const skillSortCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
141
142
  const recentOrderIndex = buildRecentOrderIndex(recentSkillValues);
143
+ const groupOrderIndex = new Map<string, number>();
144
+ for (const record of skillRecords) {
145
+ const groupKey = record.groupKey?.trim();
146
+ if (groupKey && !groupOrderIndex.has(groupKey)) {
147
+ groupOrderIndex.set(groupKey, groupOrderIndex.size);
148
+ }
149
+ }
142
150
 
143
151
  return skillRecords
144
152
  .map((record, order) => ({
@@ -148,6 +156,11 @@ export function buildChatSlashItems(
148
156
  }))
149
157
  .filter((entry) => entry.score > 0)
150
158
  .sort((left, right) => {
159
+ const leftGroupIndex = groupOrderIndex.get(left.record.groupKey?.trim() ?? '') ?? Number.POSITIVE_INFINITY;
160
+ const rightGroupIndex = groupOrderIndex.get(right.record.groupKey?.trim() ?? '') ?? Number.POSITIVE_INFINITY;
161
+ if (leftGroupIndex !== rightGroupIndex) {
162
+ return leftGroupIndex - rightGroupIndex;
163
+ }
151
164
  const leftTier = resolveSlashMatchTier(left.score);
152
165
  const rightTier = resolveSlashMatchTier(right.score);
153
166
  if (rightTier !== leftTier) {
@@ -178,6 +191,8 @@ export function buildChatSlashItems(
178
191
  `${texts.slashSkillSpecLabel}: ${record.key}`,
179
192
  ...(record.scopeLabel ? [`${texts.slashSkillScopeLabel}: ${record.scopeLabel}`] : [])
180
193
  ],
194
+ sectionKey: record.groupKey,
195
+ sectionLabel: record.groupLabel,
181
196
  tokenKind: 'skill',
182
197
  tokenKey: record.key,
183
198
  value: record.key
@@ -193,6 +208,46 @@ function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPick
193
208
  }));
194
209
  }
195
210
 
211
+ function buildSkillPickerGroups(params: {
212
+ skillRecords: ChatSkillRecord[];
213
+ recentKeySet: Set<string>;
214
+ recentSkillsLabel: string;
215
+ allSkillsLabel: string;
216
+ }): ChatSkillPickerOptionGroup[] | undefined {
217
+ const { allSkillsLabel, recentKeySet, recentSkillsLabel, skillRecords } = params;
218
+ const recentRecords = skillRecords.filter((record) => recentKeySet.has(record.key));
219
+ const groupedRecords = new Map<string, { label: string; records: ChatSkillRecord[] }>();
220
+ for (const record of skillRecords) {
221
+ if (recentKeySet.has(record.key)) {
222
+ continue;
223
+ }
224
+ const groupKey = record.groupKey?.trim() || 'all-skills';
225
+ const groupLabel = record.groupLabel?.trim() || allSkillsLabel;
226
+ const group = groupedRecords.get(groupKey) ?? { label: groupLabel, records: [] };
227
+ group.records.push(record);
228
+ groupedRecords.set(groupKey, group);
229
+ }
230
+ const sourceGroups = [...groupedRecords].map(([key, group]) => ({
231
+ key,
232
+ label: group.label,
233
+ options: buildSkillPickerOptions(group.records),
234
+ }));
235
+ const hasSourceGroups = sourceGroups.some((group) => group.key !== 'all-skills');
236
+ if (recentRecords.length === 0 && !hasSourceGroups) {
237
+ return undefined;
238
+ }
239
+ return [
240
+ ...(recentRecords.length > 0
241
+ ? [{
242
+ key: 'recent-skills',
243
+ label: recentSkillsLabel,
244
+ options: buildSkillPickerOptions(recentRecords),
245
+ }]
246
+ : []),
247
+ ...sourceGroups,
248
+ ].filter((group) => group.options.length > 0);
249
+ }
250
+
196
251
  export function buildSkillPickerModel(params: {
197
252
  skillRecords: ChatSkillRecord[];
198
253
  recentSkillValues?: string[];
@@ -221,12 +276,6 @@ export function buildSkillPickerModel(params: {
221
276
  } = params;
222
277
  const prioritizedSkillRecords = prioritizeSkillRecords(skillRecords, recentSkillValues ?? []);
223
278
  const recentKeySet = new Set(groupedRecentSkillValues ?? []);
224
- const recentSkillOptions = buildSkillPickerOptions(
225
- prioritizedSkillRecords.filter((record) => recentKeySet.has(record.key))
226
- );
227
- const remainingSkillOptions = buildSkillPickerOptions(
228
- prioritizedSkillRecords.filter((record) => !recentKeySet.has(record.key))
229
- );
230
279
  return {
231
280
  title: texts.title,
232
281
  searchPlaceholder: texts.searchPlaceholder,
@@ -236,21 +285,12 @@ export function buildSkillPickerModel(params: {
236
285
  manageLabel: texts.manageLabel,
237
286
  manageHref: '/marketplace/skills',
238
287
  options: buildSkillPickerOptions(prioritizedSkillRecords),
239
- groups:
240
- recentSkillOptions.length > 0
241
- ? [
242
- {
243
- key: 'recent-skills',
244
- label: texts.recentSkillsLabel,
245
- options: recentSkillOptions
246
- },
247
- {
248
- key: 'all-skills',
249
- label: texts.allSkillsLabel,
250
- options: remainingSkillOptions
251
- }
252
- ].filter((group) => group.options.length > 0)
253
- : undefined,
288
+ groups: buildSkillPickerGroups({
289
+ skillRecords: prioritizedSkillRecords,
290
+ recentKeySet,
291
+ recentSkillsLabel: texts.recentSkillsLabel,
292
+ allSkillsLabel: texts.allSkillsLabel,
293
+ }),
254
294
  selectedKeys: selectedSkills,
255
295
  onSelectedKeysChange
256
296
  };