@nextclaw/ui 0.24.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/.vite/manifest.json +538 -525
  3. package/dist/assets/{account-panel-BxMlLXYH.js → account-panel-UvHFa8Kl.js} +1 -1
  4. package/dist/assets/{api-Utt-xzrT.js → api-Dffeij78.js} +5 -5
  5. package/dist/assets/{app-data-DYXDL6X2.js → app-data-DzaIMy4D.js} +1 -1
  6. package/dist/assets/app-presenter-provider-O2tUKKGf.js +109 -0
  7. package/dist/assets/{appearance-settings-page-BfYznAK5.js → appearance-settings-page-D8cWniw-.js} +1 -1
  8. package/dist/assets/{book-open-DDW_3XpM.js → book-open-xqEj1OLE.js} +1 -1
  9. package/dist/assets/{boxes-DwJeuK_h.js → boxes-D6uPnkGo.js} +1 -1
  10. package/dist/assets/{card-IevY2nm-.js → card-CS8su2Zh.js} +1 -1
  11. package/dist/assets/{channels-list-page-DitnqbMR.js → channels-list-page-B_fYP1Pn.js} +1 -1
  12. package/dist/assets/{chevron-down-An7Km-t4.js → chevron-down-BL-yMYcX.js} +1 -1
  13. package/dist/assets/{circle-alert-CDXqELuo.js → circle-alert-lSa3LRvS.js} +1 -1
  14. package/dist/assets/{circle-dashed-CapXEpiV.js → circle-dashed-CU3yApvP.js} +1 -1
  15. package/dist/assets/{client-B0krol7A.js → client-BtGBwLTs.js} +1 -1
  16. package/dist/assets/{config-split-page-BtnkqWVp.js → config-split-page-6hHiiapl.js} +1 -1
  17. package/dist/assets/{confirm-dialog-DFt4jpN7.js → confirm-dialog-ClT6iaj8.js} +1 -1
  18. package/dist/assets/{createLucideIcon-BTHK0Ot9.js → createLucideIcon-BNKf0_kc.js} +1 -1
  19. package/dist/assets/{desktop-capabilities-page-CCf14WId.js → desktop-capabilities-page-CB5pniTx.js} +1 -1
  20. package/dist/assets/{desktop-update-config-DVoVgfFi.js → desktop-update-config-OuQV73gg.js} +1 -1
  21. package/dist/assets/{dialog-BFN_1DBH.js → dialog-Xe73hpQz.js} +1 -1
  22. package/dist/assets/{dist-Bc9hOVNN.js → dist-BiIcnf9j.js} +1 -1
  23. package/dist/assets/{dist-CEhIDAnr.js → dist-C5I0v8Qn.js} +1 -1
  24. package/dist/assets/{dist-hkVPP1th.js → dist-czPa8xFO.js} +1 -1
  25. package/dist/assets/doc-browser-BFkslEzY.js +1 -0
  26. package/dist/assets/{doc-browser-BNUxmwD6.js → doc-browser-Cu58uiEg.js} +1 -1
  27. package/dist/assets/{doc-browser-context-C4yQrKrH.js → doc-browser-context-CekJxJFm.js} +1 -1
  28. package/dist/assets/{ellipsis-BRI6ROeu.js → ellipsis-C5YXdOUx.js} +1 -1
  29. package/dist/assets/{es2015-MOCbM0Eu.js → es2015-cuwbYI-l.js} +1 -1
  30. package/dist/assets/{extensions-DxUBMtHW.js → extensions-B5r-0jCq.js} +1 -1
  31. package/dist/assets/{eye-cnynUDGt.js → eye-C84inPcV.js} +1 -1
  32. package/dist/assets/{form-actions-gQ0JEUwB.js → form-actions-AIdXOoCL.js} +1 -1
  33. package/dist/assets/{i18n-B9yXA8KO.js → i18n-CPjq_hQX.js} +2 -2
  34. package/dist/assets/{i18n-provider-BYBJua91.js → i18n-provider-BYO9AhFy.js} +1 -1
  35. package/dist/assets/keyboard-shortcuts-settings-page-1efEkGwq.js +1 -0
  36. package/dist/assets/loader-circle-K1BDdHR5.js +1 -0
  37. package/dist/assets/{login-page-CqrHhzaN.js → login-page-DBqkVTVD.js} +1 -1
  38. package/dist/assets/login-page-DjcRQLQe.js +1 -0
  39. package/dist/assets/{logo-badge-Cr03YceJ.js → logo-badge-C-5-zim6.js} +1 -1
  40. package/dist/assets/main-CLwCjK9U.css +1 -0
  41. package/dist/assets/main-CQ6GUYdX.js +2 -0
  42. package/dist/assets/main-i18n-catalog.config-QB4DfJRJ.js +1 -0
  43. package/dist/assets/{mcp-marketplace-page-CifiVgFD.js → mcp-marketplace-page-CWAxBNaS.js} +1 -1
  44. package/dist/assets/mcp-marketplace-page-Ct090ceA.js +1 -0
  45. package/dist/assets/{model-config-page-C6jVNbSj.js → model-config-page-DBcuZyRC.js} +1 -1
  46. package/dist/assets/{navigation-link-kbCLRSr_.js → navigation-link-BAQjgx2F.js} +1 -1
  47. package/dist/assets/panel-app-i18n-catalog.config-CQFujXqt.js +1 -0
  48. package/dist/assets/panel-app-maumVhbl.js +1 -0
  49. package/dist/assets/{panelStandalone-DMBnYyTw.js → panelStandalone-BGA1s7JG.js} +2 -2
  50. package/dist/assets/plus-LA8-jpxP.js +1 -0
  51. package/dist/assets/{popover-XVDgjxcl.js → popover-NkrFlWHp.js} +1 -1
  52. package/dist/assets/{privacy-settings-page-BN27MBVA.js → privacy-settings-page-DVC-aq1v.js} +1 -1
  53. package/dist/assets/projects-Cq0E3c7N.js +1 -0
  54. package/dist/assets/{provider-scoped-model-input-BGZMCThn.js → provider-scoped-model-input-BGdZMWWa.js} +1 -1
  55. package/dist/assets/{providers-config-page-D5aBwbOD.js → providers-config-page-B-IhHl6x.js} +1 -1
  56. package/dist/assets/{puzzle-D5Dhacmb.js → puzzle-D6gulz7u.js} +1 -1
  57. package/dist/assets/{pwa-ClQK56Qe.js → pwa-C3kR9mdy.js} +1 -1
  58. package/dist/assets/{refresh-cw-Bw5BY7vp.js → refresh-cw-h9Xq3LY3.js} +1 -1
  59. package/dist/assets/remote-Cnawibbg.js +1 -0
  60. package/dist/assets/{rotate-cw-BksCLAeE.js → rotate-cw-B6xm41Wn.js} +1 -1
  61. package/dist/assets/runtime-config-page-LmrB2q65.js +1 -0
  62. package/dist/assets/{save-CVi8VWzb.js → save-fff_WpY2.js} +1 -1
  63. package/dist/assets/{search-DN-f7IIC.js → search-CUe41wjm.js} +1 -1
  64. package/dist/assets/{search-config-page-BmuZTVxs.js → search-config-page-BIXmv5FY.js} +1 -1
  65. package/dist/assets/{secrets-config-page-NPHpg9sb.js → secrets-config-page-DU3fadrq.js} +1 -1
  66. package/dist/assets/security-config-9s0i0I4N.js +1 -0
  67. package/dist/assets/{select-CmDTqk2C.js → select-CG5YWnwe.js} +1 -1
  68. package/dist/assets/{service-apps-panel-BYGJ9_pO.js → service-apps-panel-Bshf3Wmq.js} +1 -1
  69. package/dist/assets/{settings-2-B62YtZon.js → settings-2-CE3ELQyk.js} +1 -1
  70. package/dist/assets/skeleton-BL9h32W7.js +1 -0
  71. package/dist/assets/{tabs-Bk_mG7ql.js → tabs-BuE0qKMh.js} +1 -1
  72. package/dist/assets/{tag-chip-DgqKlI5D.js → tag-chip-gAH1IOsZ.js} +1 -1
  73. package/dist/assets/{theme-provider-CeBY2pyu.js → theme-provider-DE7m9jvf.js} +1 -1
  74. package/dist/assets/{tooltip-CgGbBdYf.js → tooltip-DYcuWhuP.js} +1 -1
  75. package/dist/assets/{trash-2-CVOev8yc.js → trash-2-BPPYt2NV.js} +1 -1
  76. package/dist/assets/{use-config-b2DN67gs.js → use-config-Cyiw06-j.js} +1 -1
  77. package/dist/assets/{use-desktop-capabilities-B3tLU1yl.js → use-desktop-capabilities-OKG96v4y.js} +1 -1
  78. package/dist/assets/{use-panel-app-scroll-restoration-BekzoX6z.js → use-panel-app-scroll-restoration-DsV1IJx-.js} +1 -1
  79. package/dist/assets/{use-viewport-layout-CUA3jQsA.js → use-viewport-layout-BS18drMT.js} +1 -1
  80. package/dist/assets/{utils-UBHTdG5Q.js → utils-CBZPzC_b.js} +1 -1
  81. package/dist/assets/x-DdXOqLnX.js +1 -0
  82. package/dist/index.html +50 -50
  83. package/dist/panel-standalone.html +15 -15
  84. package/package.json +7 -7
  85. package/src/app/configs/app-navigation.config.ts +10 -3
  86. package/src/app/index.tsx +7 -0
  87. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  88. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +12 -0
  89. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +13 -1
  90. package/src/features/chat/features/conversation/components/__tests__/session-voice-input.test-utils.ts +52 -0
  91. package/src/features/chat/features/conversation/components/chat-voice-input-panel.tsx +70 -0
  92. package/src/features/chat/features/conversation/components/chat-voice-shortcut-control.tsx +33 -0
  93. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +1 -11
  94. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +29 -5
  95. package/src/features/chat/features/conversation/hooks/__tests__/use-chat-voice-input.test.tsx +185 -0
  96. package/src/features/chat/features/conversation/hooks/use-chat-voice-input.ts +52 -0
  97. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +13 -6
  98. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +28 -12
  99. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +19 -1
  100. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +10 -0
  101. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +7 -6
  102. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +4 -1
  103. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +20 -0
  104. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +90 -6
  105. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +64 -2
  106. package/src/features/chat/index.ts +1 -0
  107. package/src/features/chat/managers/chat-voice-input.manager.ts +174 -0
  108. package/src/features/chat/presenters/chat.presenter.ts +2 -0
  109. package/src/features/chat/stores/chat-voice-shortcut.store.ts +31 -0
  110. package/src/features/desktop-capabilities/index.ts +1 -0
  111. package/src/features/panel-apps/components/__tests__/panel-app-open-standalone-menu-item.test.tsx +3 -2
  112. package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +4 -1
  113. package/src/features/projects/AGENTS.md +1 -2
  114. package/src/features/projects/components/project-agreement.tsx +36 -77
  115. package/src/features/projects/components/project-artifacts.test.tsx +90 -62
  116. package/src/features/projects/components/project-artifacts.tsx +81 -160
  117. package/src/features/projects/components/project-materials.test.tsx +80 -0
  118. package/src/features/projects/components/project-overview.test.tsx +50 -40
  119. package/src/features/projects/components/project-overview.tsx +5 -4
  120. package/src/features/projects/components/project-skills.tsx +24 -14
  121. package/src/features/projects/components/work/project-work-item-drawer.test.tsx +3 -2
  122. package/src/features/projects/components/work/project-work-items.test.tsx +13 -1
  123. package/src/features/projects/components/work/project-work-items.tsx +7 -7
  124. package/src/features/projects/hooks/use-project-materials.test.tsx +65 -0
  125. package/src/features/projects/hooks/use-project-materials.ts +24 -0
  126. package/src/features/projects/hooks/use-project-work.test.tsx +57 -1
  127. package/src/features/projects/hooks/use-project-work.ts +11 -5
  128. package/src/features/projects/pages/project-home-page.tsx +51 -127
  129. package/src/features/projects/pages/projects-page.test.tsx +68 -75
  130. package/src/features/projects/presenters/project-home.presenter.test.ts +1 -64
  131. package/src/features/projects/presenters/project-home.presenter.ts +17 -54
  132. package/src/features/projects/utils/project-artifact-view.utils.test.ts +0 -46
  133. package/src/features/projects/utils/project-artifact-view.utils.ts +0 -47
  134. package/src/features/settings/pages/keyboard-shortcuts-settings-page.tsx +15 -0
  135. package/src/platforms/mobile/components/mobile-settings-shell.tsx +2 -1
  136. package/src/shared/lib/api/__tests__/ncp-session-query-cache.utils.test.ts +109 -1
  137. package/src/shared/lib/api/ncp-session-query-cache.utils.ts +71 -2
  138. package/src/shared/lib/i18n/locales/en-US/chat.json +32 -1
  139. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  140. package/src/shared/lib/i18n/locales/en-US/projects.json +8 -59
  141. package/src/shared/lib/i18n/locales/zh-CN/chat.json +32 -1
  142. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  143. package/src/shared/lib/i18n/locales/zh-CN/projects.json +8 -59
  144. package/src/shared/lib/transport/__tests__/browser-realtime-recovery.service.test.ts +42 -0
  145. package/src/shared/lib/transport/__tests__/local-transport.service.test.ts +85 -0
  146. package/src/shared/lib/transport/__tests__/remote.transport.test.ts +54 -1
  147. package/src/shared/lib/transport/browser-realtime-recovery.service.ts +35 -0
  148. package/src/shared/lib/transport/local-transport.service.ts +32 -0
  149. package/src/shared/lib/transport/remote-transport.service.ts +30 -0
  150. package/dist/assets/app-presenter-provider-CLsFiXvV.js +0 -109
  151. package/dist/assets/doc-browser-w4eGZXLu.js +0 -1
  152. package/dist/assets/loader-circle-rWr5Yo0E.js +0 -1
  153. package/dist/assets/login-page-CfMSkOsg.js +0 -1
  154. package/dist/assets/main-8cVkMfgE.css +0 -1
  155. package/dist/assets/main-Bys58GxY.js +0 -2
  156. package/dist/assets/main-i18n-catalog.config-BSq9yiUQ.js +0 -1
  157. package/dist/assets/mcp-marketplace-page-DX5VE3mU.js +0 -1
  158. package/dist/assets/panel-app-BUUle0gP.js +0 -1
  159. package/dist/assets/panel-app-i18n-catalog.config-C2St67SP.js +0 -1
  160. package/dist/assets/plus-BHnKwuPq.js +0 -1
  161. package/dist/assets/projects-DV3UuJMS.js +0 -1
  162. package/dist/assets/remote-Bl005fzu.js +0 -1
  163. package/dist/assets/runtime-config-page-CmktQcMW.js +0 -1
  164. package/dist/assets/security-config-Djptuyf2.js +0 -1
  165. package/dist/assets/skeleton-Eh64l4T2.js +0 -1
  166. package/dist/assets/x-C0iaOsRW.js +0 -1
  167. package/src/features/projects/components/project-requests.tsx +0 -57
  168. package/src/features/projects/components/project-runs.tsx +0 -60
  169. package/src/features/projects/hooks/use-project-observation.test.tsx +0 -93
  170. package/src/features/projects/hooks/use-project-observation.ts +0 -115
  171. package/src/features/projects/utils/project-request-response.utils.test.ts +0 -44
  172. package/src/features/projects/utils/project-request-response.utils.ts +0 -39
@@ -0,0 +1 @@
1
+ import{t as e}from"./createLucideIcon-BNKf0_kc.js";var t=e(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]);export{t};
package/dist/index.html CHANGED
@@ -258,62 +258,62 @@
258
258
  })();
259
259
  </script>
260
260
  <title>NextClaw</title>
261
- <script type="module" crossorigin src="/assets/main-Bys58GxY.js"></script>
261
+ <script type="module" crossorigin src="/assets/main-CQ6GUYdX.js"></script>
262
262
  <link rel="modulepreload" crossorigin href="/assets/chunk-DF37yw5_.js">
263
- <link rel="modulepreload" crossorigin href="/assets/utils-UBHTdG5Q.js">
264
- <link rel="modulepreload" crossorigin href="/assets/client-B0krol7A.js">
263
+ <link rel="modulepreload" crossorigin href="/assets/utils-CBZPzC_b.js">
264
+ <link rel="modulepreload" crossorigin href="/assets/client-BtGBwLTs.js">
265
265
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-ca-nBW7U.js">
266
- <link rel="modulepreload" crossorigin href="/assets/dist-Bc9hOVNN.js">
267
- <link rel="modulepreload" crossorigin href="/assets/dist-CEhIDAnr.js">
268
- <link rel="modulepreload" crossorigin href="/assets/es2015-MOCbM0Eu.js">
269
- <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BTHK0Ot9.js">
270
- <link rel="modulepreload" crossorigin href="/assets/chevron-down-An7Km-t4.js">
271
- <link rel="modulepreload" crossorigin href="/assets/select-CmDTqk2C.js">
272
- <link rel="modulepreload" crossorigin href="/assets/dist-hkVPP1th.js">
273
- <link rel="modulepreload" crossorigin href="/assets/x-C0iaOsRW.js">
274
- <link rel="modulepreload" crossorigin href="/assets/dialog-BFN_1DBH.js">
275
- <link rel="modulepreload" crossorigin href="/assets/popover-XVDgjxcl.js">
276
- <link rel="modulepreload" crossorigin href="/assets/tooltip-CgGbBdYf.js">
277
- <link rel="modulepreload" crossorigin href="/assets/i18n-B9yXA8KO.js">
278
- <link rel="modulepreload" crossorigin href="/assets/api-Utt-xzrT.js">
279
- <link rel="modulepreload" crossorigin href="/assets/search-DN-f7IIC.js">
280
- <link rel="modulepreload" crossorigin href="/assets/book-open-DDW_3XpM.js">
281
- <link rel="modulepreload" crossorigin href="/assets/boxes-DwJeuK_h.js">
266
+ <link rel="modulepreload" crossorigin href="/assets/dist-BiIcnf9j.js">
267
+ <link rel="modulepreload" crossorigin href="/assets/dist-C5I0v8Qn.js">
268
+ <link rel="modulepreload" crossorigin href="/assets/es2015-cuwbYI-l.js">
269
+ <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BNKf0_kc.js">
270
+ <link rel="modulepreload" crossorigin href="/assets/chevron-down-BL-yMYcX.js">
271
+ <link rel="modulepreload" crossorigin href="/assets/select-CG5YWnwe.js">
272
+ <link rel="modulepreload" crossorigin href="/assets/dist-czPa8xFO.js">
273
+ <link rel="modulepreload" crossorigin href="/assets/x-DdXOqLnX.js">
274
+ <link rel="modulepreload" crossorigin href="/assets/dialog-Xe73hpQz.js">
275
+ <link rel="modulepreload" crossorigin href="/assets/popover-NkrFlWHp.js">
276
+ <link rel="modulepreload" crossorigin href="/assets/tooltip-DYcuWhuP.js">
277
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CPjq_hQX.js">
278
+ <link rel="modulepreload" crossorigin href="/assets/api-Dffeij78.js">
279
+ <link rel="modulepreload" crossorigin href="/assets/search-CUe41wjm.js">
280
+ <link rel="modulepreload" crossorigin href="/assets/book-open-xqEj1OLE.js">
281
+ <link rel="modulepreload" crossorigin href="/assets/boxes-D6uPnkGo.js">
282
282
  <link rel="modulepreload" crossorigin href="/assets/host-capabilities-B107nwS3.js">
283
- <link rel="modulepreload" crossorigin href="/assets/navigation-link-kbCLRSr_.js">
284
- <link rel="modulepreload" crossorigin href="/assets/plus-BHnKwuPq.js">
285
- <link rel="modulepreload" crossorigin href="/assets/refresh-cw-Bw5BY7vp.js">
283
+ <link rel="modulepreload" crossorigin href="/assets/navigation-link-BAQjgx2F.js">
284
+ <link rel="modulepreload" crossorigin href="/assets/plus-LA8-jpxP.js">
285
+ <link rel="modulepreload" crossorigin href="/assets/refresh-cw-h9Xq3LY3.js">
286
286
  <link rel="modulepreload" crossorigin href="/assets/middleware-0o3JF3tS.js">
287
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-C4yQrKrH.js">
288
- <link rel="modulepreload" crossorigin href="/assets/use-panel-app-scroll-restoration-BekzoX6z.js">
289
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-BNUxmwD6.js">
290
- <link rel="modulepreload" crossorigin href="/assets/circle-alert-CDXqELuo.js">
291
- <link rel="modulepreload" crossorigin href="/assets/loader-circle-rWr5Yo0E.js">
292
- <link rel="modulepreload" crossorigin href="/assets/confirm-dialog-DFt4jpN7.js">
293
- <link rel="modulepreload" crossorigin href="/assets/i18n-provider-BYBJua91.js">
294
- <link rel="modulepreload" crossorigin href="/assets/tag-chip-DgqKlI5D.js">
295
- <link rel="modulepreload" crossorigin href="/assets/skeleton-Eh64l4T2.js">
296
- <link rel="modulepreload" crossorigin href="/assets/mcp-marketplace-page-CifiVgFD.js">
297
- <link rel="modulepreload" crossorigin href="/assets/puzzle-D5Dhacmb.js">
298
- <link rel="modulepreload" crossorigin href="/assets/trash-2-CVOev8yc.js">
299
- <link rel="modulepreload" crossorigin href="/assets/app-data-DYXDL6X2.js">
300
- <link rel="modulepreload" crossorigin href="/assets/ellipsis-BRI6ROeu.js">
301
- <link rel="modulepreload" crossorigin href="/assets/eye-cnynUDGt.js">
302
- <link rel="modulepreload" crossorigin href="/assets/tabs-Bk_mG7ql.js">
303
- <link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-CUA3jQsA.js">
304
- <link rel="modulepreload" crossorigin href="/assets/theme-provider-CeBY2pyu.js">
305
- <link rel="modulepreload" crossorigin href="/assets/use-desktop-capabilities-B3tLU1yl.js">
306
- <link rel="modulepreload" crossorigin href="/assets/logo-badge-Cr03YceJ.js">
307
- <link rel="modulepreload" crossorigin href="/assets/card-IevY2nm-.js">
308
- <link rel="modulepreload" crossorigin href="/assets/use-config-b2DN67gs.js">
287
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-CekJxJFm.js">
288
+ <link rel="modulepreload" crossorigin href="/assets/use-panel-app-scroll-restoration-DsV1IJx-.js">
289
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-Cu58uiEg.js">
290
+ <link rel="modulepreload" crossorigin href="/assets/circle-alert-lSa3LRvS.js">
291
+ <link rel="modulepreload" crossorigin href="/assets/loader-circle-K1BDdHR5.js">
292
+ <link rel="modulepreload" crossorigin href="/assets/confirm-dialog-ClT6iaj8.js">
293
+ <link rel="modulepreload" crossorigin href="/assets/i18n-provider-BYO9AhFy.js">
294
+ <link rel="modulepreload" crossorigin href="/assets/tag-chip-gAH1IOsZ.js">
295
+ <link rel="modulepreload" crossorigin href="/assets/skeleton-BL9h32W7.js">
296
+ <link rel="modulepreload" crossorigin href="/assets/mcp-marketplace-page-CWAxBNaS.js">
297
+ <link rel="modulepreload" crossorigin href="/assets/puzzle-D6gulz7u.js">
298
+ <link rel="modulepreload" crossorigin href="/assets/trash-2-BPPYt2NV.js">
299
+ <link rel="modulepreload" crossorigin href="/assets/app-data-DzaIMy4D.js">
300
+ <link rel="modulepreload" crossorigin href="/assets/ellipsis-C5YXdOUx.js">
301
+ <link rel="modulepreload" crossorigin href="/assets/eye-C84inPcV.js">
302
+ <link rel="modulepreload" crossorigin href="/assets/tabs-BuE0qKMh.js">
303
+ <link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-BS18drMT.js">
304
+ <link rel="modulepreload" crossorigin href="/assets/theme-provider-DE7m9jvf.js">
305
+ <link rel="modulepreload" crossorigin href="/assets/use-desktop-capabilities-OKG96v4y.js">
306
+ <link rel="modulepreload" crossorigin href="/assets/logo-badge-C-5-zim6.js">
307
+ <link rel="modulepreload" crossorigin href="/assets/card-CS8su2Zh.js">
308
+ <link rel="modulepreload" crossorigin href="/assets/use-config-Cyiw06-j.js">
309
309
  <link rel="modulepreload" crossorigin href="/assets/provider-models-5PGHb6mP.js">
310
- <link rel="modulepreload" crossorigin href="/assets/app-presenter-provider-CLsFiXvV.js">
311
- <link rel="modulepreload" crossorigin href="/assets/provider-scoped-model-input-BGZMCThn.js">
312
- <link rel="modulepreload" crossorigin href="/assets/settings-2-B62YtZon.js">
313
- <link rel="modulepreload" crossorigin href="/assets/pwa-ClQK56Qe.js">
314
- <link rel="modulepreload" crossorigin href="/assets/login-page-CqrHhzaN.js">
310
+ <link rel="modulepreload" crossorigin href="/assets/app-presenter-provider-O2tUKKGf.js">
311
+ <link rel="modulepreload" crossorigin href="/assets/provider-scoped-model-input-BGdZMWWa.js">
312
+ <link rel="modulepreload" crossorigin href="/assets/settings-2-CE3ELQyk.js">
313
+ <link rel="modulepreload" crossorigin href="/assets/pwa-C3kR9mdy.js">
314
+ <link rel="modulepreload" crossorigin href="/assets/login-page-DBqkVTVD.js">
315
315
  <link rel="stylesheet" crossorigin href="/assets/app-presenter-provider-D3BuuOyZ.css">
316
- <link rel="stylesheet" crossorigin href="/assets/main-8cVkMfgE.css">
316
+ <link rel="stylesheet" crossorigin href="/assets/main-CLwCjK9U.css">
317
317
  </head>
318
318
 
319
319
  <body>
@@ -5,23 +5,23 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#ffffff" />
7
7
  <title>Panel App · NextClaw</title>
8
- <script type="module" crossorigin src="/assets/panelStandalone-DMBnYyTw.js"></script>
8
+ <script type="module" crossorigin src="/assets/panelStandalone-BGA1s7JG.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-DF37yw5_.js">
10
- <link rel="modulepreload" crossorigin href="/assets/utils-UBHTdG5Q.js">
11
- <link rel="modulepreload" crossorigin href="/assets/client-B0krol7A.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/utils-CBZPzC_b.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/client-BtGBwLTs.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-ca-nBW7U.js">
13
- <link rel="modulepreload" crossorigin href="/assets/i18n-B9yXA8KO.js">
14
- <link rel="modulepreload" crossorigin href="/assets/api-Utt-xzrT.js">
15
- <link rel="modulepreload" crossorigin href="/assets/dist-Bc9hOVNN.js">
16
- <link rel="modulepreload" crossorigin href="/assets/es2015-MOCbM0Eu.js">
17
- <link rel="modulepreload" crossorigin href="/assets/dist-hkVPP1th.js">
18
- <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BTHK0Ot9.js">
19
- <link rel="modulepreload" crossorigin href="/assets/x-C0iaOsRW.js">
20
- <link rel="modulepreload" crossorigin href="/assets/dialog-BFN_1DBH.js">
21
- <link rel="modulepreload" crossorigin href="/assets/boxes-DwJeuK_h.js">
22
- <link rel="modulepreload" crossorigin href="/assets/use-panel-app-scroll-restoration-BekzoX6z.js">
23
- <link rel="modulepreload" crossorigin href="/assets/theme-provider-CeBY2pyu.js">
24
- <link rel="modulepreload" crossorigin href="/assets/i18n-provider-BYBJua91.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CPjq_hQX.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/api-Dffeij78.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/dist-BiIcnf9j.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/es2015-cuwbYI-l.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/dist-czPa8xFO.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BNKf0_kc.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/x-DdXOqLnX.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/dialog-Xe73hpQz.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/boxes-D6uPnkGo.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/use-panel-app-scroll-restoration-DsV1IJx-.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/theme-provider-DE7m9jvf.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/i18n-provider-BYO9AhFy.js">
25
25
  <link rel="stylesheet" crossorigin href="/assets/panelStandalone-Y91OJkgV.css">
26
26
  </head>
27
27
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,14 +34,14 @@
34
34
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
35
35
  "zod": "^3.23.8",
36
36
  "zustand": "^5.0.2",
37
- "@nextclaw/agent-chat": "0.3.14",
38
- "@nextclaw/agent-chat-ui": "0.7.2",
39
- "@nextclaw/client-sdk": "0.11.2",
40
37
  "@nextclaw/ncp": "0.10.0",
38
+ "@nextclaw/agent-chat": "0.3.14",
41
39
  "@nextclaw/ncp-http-agent-client": "0.4.22",
42
- "@nextclaw/ncp-react": "0.5.26",
43
- "@nextclaw/ncp-toolkit": "0.6.23",
44
- "@nextclaw/shared": "0.5.0"
40
+ "@nextclaw/agent-chat-ui": "0.8.0",
41
+ "@nextclaw/ncp-react": "0.5.27",
42
+ "@nextclaw/client-sdk": "0.12.1",
43
+ "@nextclaw/ncp-toolkit": "0.6.24",
44
+ "@nextclaw/shared": "0.5.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/react": "^16.3.0",
@@ -8,6 +8,7 @@ import {
8
8
  Download,
9
9
  Inbox,
10
10
  KeyRound,
11
+ Keyboard,
11
12
  MessageCircle,
12
13
  MessageSquare,
13
14
  MonitorCog,
@@ -35,6 +36,7 @@ export type AppNavigationSection = {
35
36
  };
36
37
 
37
38
  type SettingsNavigationOptions = {
39
+ includeKeyboardShortcuts?: boolean;
38
40
  includeDesktopCapabilities?: boolean;
39
41
  };
40
42
 
@@ -165,6 +167,11 @@ export function getSettingsNavItems(
165
167
  label: translate("appearance"),
166
168
  icon: Palette,
167
169
  },
170
+ {
171
+ target: "/keyboard-shortcuts",
172
+ label: translate("keyboardShortcuts"),
173
+ icon: Keyboard,
174
+ },
168
175
  {
169
176
  target: "/security",
170
177
  label: translate("security"),
@@ -211,9 +218,9 @@ export function getSettingsNavItems(
211
218
  icon: Wrench,
212
219
  },
213
220
  ];
214
- return options.includeDesktopCapabilities === false
215
- ? items.filter((item) => item.target !== "/desktop-capabilities")
216
- : items;
221
+ return items.filter((item) =>
222
+ (options.includeDesktopCapabilities !== false || item.target !== "/desktop-capabilities") &&
223
+ (options.includeKeyboardShortcuts !== false || item.target !== "/keyboard-shortcuts"));
217
224
  }
218
225
 
219
226
  export function getSettingsNavSections(
package/src/app/index.tsx CHANGED
@@ -42,6 +42,9 @@ const AppearanceSettingsPage = lazy(async () => ({
42
42
  default: (await import("@/features/settings/pages/appearance-settings-page"))
43
43
  .AppearanceSettingsPage,
44
44
  }));
45
+ const KeyboardShortcutsSettingsPage = lazy(async () => ({
46
+ default: (await import("@/features/settings/pages/keyboard-shortcuts-settings-page")).KeyboardShortcutsSettingsPage,
47
+ }));
45
48
  const SearchConfigPage = lazy(async () => ({
46
49
  default: (await import("@/features/settings/pages/search-config-page"))
47
50
  .SearchConfigPage,
@@ -157,6 +160,10 @@ const protectedRouteDefinitions: ProtectedRouteDefinition[] = [
157
160
  path: "/appearance",
158
161
  element: createLazyElement(<AppearanceSettingsPage />),
159
162
  },
163
+ {
164
+ path: "/keyboard-shortcuts",
165
+ element: createLazyElement(<KeyboardShortcutsSettingsPage />),
166
+ },
160
167
  {
161
168
  path: "/model",
162
169
  element: createLazyElement(<ModelConfigPage />),
@@ -6,12 +6,14 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
6
6
  import type { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
7
7
  import type { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
8
8
  import type { ChatComposerIntentManager } from '@/features/chat/managers/chat-composer-intent.manager';
9
+ import type { ChatVoiceInputManager } from '@/features/chat/managers/chat-voice-input.manager';
9
10
 
10
11
  type PublicManager<T extends object> = Pick<T, keyof T>;
11
12
 
12
13
  export type ChatThreadManagerLike = PublicManager<ChatThreadManager>;
13
14
 
14
15
  export type ChatPresenterLike = {
16
+ chatVoiceInputManager: ChatVoiceInputManager;
15
17
  chatUiManager: ChatUiManager;
16
18
  chatQueryManager: ChatQueryManager;
17
19
  chatSessionListManager: ChatSessionListManager;
@@ -309,6 +309,18 @@ describe("SessionConversationArea input boundary", () => {
309
309
  expect(params.agent.isSending).toBe(false);
310
310
  });
311
311
 
312
+ it("does not keep a settled run active because the session list cache is stale", () => {
313
+ mocks.inputQuery.selectedSession = { status: "running" };
314
+ mocks.agent.isRunning = false;
315
+ mocks.agent.isSending = false;
316
+ renderArea("session-1");
317
+ const params = mocks.controllerParamsSpy.mock.calls.at(-1)?.[0] as {
318
+ agent: { isRunning: boolean; isSending: boolean };
319
+ };
320
+ expect(params.agent.isRunning).toBe(false);
321
+ expect(params.agent.isSending).toBe(false);
322
+ });
323
+
312
324
  it("disables message recovery actions until the NCP agent is ready", () => {
313
325
  mocks.agent.isRunning = false;
314
326
  mocks.agent.isSending = false;
@@ -31,6 +31,8 @@ import { useSessionConversationInputState } from '@/features/chat/features/conve
31
31
  import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
32
32
  import { useChatComposerDraftStore } from '@/features/chat/stores/chat-composer-draft.store';
33
33
  import { ChatComposerIntentManager } from '@/features/chat/managers/chat-composer-intent.manager';
34
+ import { verifyVoiceDraftLifecycle } from './session-voice-input.test-utils';
35
+ import { ChatVoiceInputManager } from '@/features/chat/managers/chat-voice-input.manager';
34
36
  import type { ThinkingLevel } from '@/shared/lib/api';
35
37
 
36
38
  const uploadNcpAssetsMock = vi.hoisted(() => vi.fn());
@@ -221,6 +223,7 @@ type StreamingInputControl = {
221
223
 
222
224
  const chatComposerIntentManager = new ChatComposerIntentManager();
223
225
  const presenter = {
226
+ chatVoiceInputManager: new ChatVoiceInputManager(),
224
227
  chatComposerIntentManager,
225
228
  chatThreadManager: {
226
229
  openSideChatDraft: vi.fn(),
@@ -270,6 +273,13 @@ function createStreamingInputSnapshot(
270
273
  };
271
274
  }
272
275
 
276
+ it('dictation uses the latest draft and never sends; cancel discards late results', async () => {
277
+ await verifyVoiceDraftLifecycle((voiceManager, send) => renderInput(
278
+ <StreamingSessionConversationInputHarness voiceManager={voiceManager} controlRef={{ current: null }}
279
+ initialText='Original' controllerOverride={{ ...controller, send, sendDisabled: false }} />,
280
+ ));
281
+ });
282
+
273
283
  function setImeDomText(textbox: HTMLElement, text: string): HTMLParagraphElement {
274
284
  const paragraph = textbox.querySelector('p');
275
285
  if (!paragraph) {
@@ -307,12 +317,14 @@ async function insertText(textbox: HTMLElement, text: string): Promise<void> {
307
317
  }
308
318
 
309
319
  function StreamingSessionConversationInputHarness({
320
+ voiceManager = presenter.chatVoiceInputManager,
310
321
  controllerOverride = controller,
311
322
  controlRef,
312
323
  initialText = '',
313
324
  sendError = null,
314
325
  }: {
315
326
  controllerOverride?: SessionConversationInputController;
327
+ voiceManager?: ChatVoiceInputManager;
316
328
  controlRef: MutableRefObject<StreamingInputControl | null>;
317
329
  initialText?: string;
318
330
  sendError?: string | null;
@@ -389,7 +401,7 @@ function StreamingSessionConversationInputHarness({
389
401
 
390
402
  return (
391
403
  <I18nProvider>
392
- <ChatPresenterProvider presenter={presenter}>
404
+ <ChatPresenterProvider presenter={{ ...presenter, chatVoiceInputManager: voiceManager }}>
393
405
  <div data-testid="stream-chunk">{streamChunk}</div>
394
406
  <SessionConversationInput
395
407
  contextWindow={null}
@@ -0,0 +1,52 @@
1
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react';
2
+ import { expect, vi } from 'vitest';
3
+ import { ChatVoiceInputManager, type SpeechRecognitionLike } from '@/features/chat/managers/chat-voice-input.manager';
4
+
5
+ /** Assembled voice/draft lifecycle check, shared with the real streaming input harness. */
6
+ export async function verifyVoiceDraftLifecycle(
7
+ renderHarness: (manager: ChatVoiceInputManager, send: () => void) => { unmount: () => void },
8
+ ) {
9
+ const recognition: SpeechRecognitionLike = {
10
+ lang: '', continuous: false, interimResults: false,
11
+ start: vi.fn(), stop: vi.fn(), abort: vi.fn(),
12
+ onstart: null, onend: null, onresult: null, onerror: null,
13
+ };
14
+ const manager = new ChatVoiceInputManager(() => recognition);
15
+ const send = vi.fn();
16
+ const rendered = renderHarness(manager, send);
17
+ try {
18
+ fireEvent.click(screen.getByRole('button', { name: /^(语音输入|Voice input)$/ }));
19
+ act(() => { recognition.onstart?.(); });
20
+ const textbox = screen.getByRole('textbox');
21
+ const before = textbox.textContent;
22
+ act(() => { recognition.onresult?.({ results: [{ isFinal: false, 0: { transcript: 'speaking' } }] }); });
23
+ await waitFor(() => expect(textbox.textContent).toContain('speaking'));
24
+ expect(textbox.querySelector('[style*="underline"]')?.textContent).toBe('speaking');
25
+ act(() => { recognition.onresult?.({ results: [{ isFinal: false, 0: { transcript: 'speak' } }] }); });
26
+ await waitFor(() => expect(textbox.textContent).not.toContain('speaking'));
27
+ act(() => { recognition.onresult?.({ results: [{ isFinal: true, 0: { transcript: 'spoken' } }] }); });
28
+ fireEvent.click(screen.getAllByRole('button', { name: /结束录音|Finish recording/ })[0]);
29
+ act(() => { recognition.onend?.(); });
30
+ await waitFor(() => expect(textbox.textContent).toContain('spoken'));
31
+ expect(textbox.textContent).toContain(before);
32
+ expect(textbox.querySelector('[style*="underline"]')).toBeNull();
33
+ expect(send).not.toHaveBeenCalled();
34
+ fireEvent.click(screen.getByRole('button', { name: /^(语音输入|Voice input)$/ }));
35
+ const lateResult = recognition.onresult;
36
+ const lateEnd = recognition.onend;
37
+ fireEvent.click(screen.getAllByRole('button', { name: /^(取消|Cancel)$/ })[0]);
38
+ act(() => {
39
+ lateResult?.({ results: [{ isFinal: true, 0: { transcript: 'discard' } }] }); lateEnd?.();
40
+ });
41
+ expect(textbox.textContent).not.toContain('discard');
42
+ fireEvent.click(screen.getByRole('button', { name: /^(语音输入|Voice input)$/ }));
43
+ act(() => { recognition.onstart?.(); });
44
+ fireEvent.click(screen.getByRole('button', { name: /^(语音快捷键|Voice shortcut)$/ }));
45
+ act(() => { recognition.onerror?.({ error: 'no-speech' }); });
46
+ await waitFor(() => expect(screen.getByRole('button', { name: /绑定按键|Bind a key/ })).toBeTruthy());
47
+ expect(screen.queryByRole('alert')).toBeNull();
48
+ expect(screen.queryByRole('button', { name: /^(重试|Retry)$/ })).toBeNull();
49
+ } finally {
50
+ rendered.unmount();
51
+ }
52
+ }
@@ -0,0 +1,70 @@
1
+ import { Mic, Square, X, LoaderCircle, Settings } from 'lucide-react';
2
+ import { Button } from '@/shared/components/ui/button';
3
+ import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
4
+ import { t } from '@/shared/lib/i18n';
5
+ import type { ChatVoiceInputManager, VoiceError, VoiceSnapshot } from '@/features/chat/managers/chat-voice-input.manager';
6
+ import { ChatVoiceShortcutControl } from './chat-voice-shortcut-control';
7
+
8
+ const ERROR_KEYS: Record<VoiceError, string> = {
9
+ unsupported: 'chatInputVoiceUnsupported', permission: 'chatInputVoicePermissionDenied',
10
+ network: 'chatInputVoiceNetwork', 'audio-capture': 'chatInputVoiceDevice',
11
+ 'no-speech': 'chatInputVoiceNoSpeech', interrupted: 'chatInputVoiceInterrupted',
12
+ timeout: 'chatInputVoiceTimeout', failed: 'chatInputVoiceFailed',
13
+ };
14
+
15
+ export function ChatVoiceInputPanel({ manager, snapshot, onStart, desktop }: {
16
+ manager: ChatVoiceInputManager;
17
+ snapshot: VoiceSnapshot;
18
+ onStart: () => void;
19
+ desktop: boolean;
20
+ }) {
21
+ const { phase, error, seconds } = snapshot;
22
+ if (phase === 'ready') return desktop ? <VoiceShortcutPanel onClose={manager.cancel} /> : null;
23
+ const busy = phase === 'starting' || phase === 'stopping';
24
+ const status = {
25
+ idle: 'chatInputVoice', ready: 'chatInputVoiceShortcut', starting: 'chatInputVoiceStarting',
26
+ recording: 'chatInputVoiceRecording', stopping: 'chatInputVoiceFinishing', error: 'chatInputVoiceFailed',
27
+ }[phase];
28
+ return (
29
+ <div role='region' aria-label={t('chatInputVoice')}
30
+ className={'max-h-[var(--radix-popover-content-available-height,24rem)] max-w-full overflow-y-auto border border-border bg-popover text-popover-foreground shadow-lg ' +
31
+ (error ? 'w-80 rounded-xl p-3' : 'rounded-full px-3 py-1.5')}
32
+ onKeyDown={(event) => {
33
+ if (event.key === 'Escape' && !event.nativeEvent.isComposing) {
34
+ event.stopPropagation(); manager.cancel();
35
+ }
36
+ }}>
37
+ <div className='flex items-center gap-2'>
38
+ {busy ? <LoaderCircle className='h-4 w-4 shrink-0 motion-safe:animate-spin text-muted-foreground' />
39
+ : <Mic className={'h-4 w-4 shrink-0 ' + (phase === 'recording' ? 'text-destructive' : '')} />}
40
+ <span role='status' className='min-w-0 flex-1 text-xs font-medium'>{t(status)}</span>
41
+ {phase === 'recording' ? <span className='text-xs tabular-nums text-muted-foreground'>{seconds}s</span> : null}
42
+ {phase === 'recording' ? <IconActionButton icon={<Square className='h-3 w-3' />}
43
+ label={t('chatInputVoiceFinish')} onClick={manager.finish} /> : null}
44
+ <IconActionButton icon={<X className='h-4 w-4' />} label={t('chatInputVoiceCancel')} onClick={manager.cancel} />
45
+ {desktop ? <IconActionButton icon={<Settings className='h-4 w-4' />}
46
+ label={t('chatInputVoiceShortcut')} disabled={busy} onClick={manager.openSettings} /> : null}
47
+ </div>
48
+ {error ? <>
49
+ <p role='alert' className='mt-2 text-xs leading-5 text-destructive'>{t(ERROR_KEYS[error])}</p>
50
+ <p className='mt-1 text-xs text-muted-foreground'>{t('chatInputVoiceReviewHint')}</p>
51
+ <Button className='mt-2' size='sm' disabled={error === 'unsupported'} onClick={onStart}>{t('chatInputVoiceRetry')}</Button>
52
+ </> : null}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ function VoiceShortcutPanel({ onClose }: { onClose: () => void }) {
58
+ return <div role='region' aria-label={t('chatInputVoiceShortcut')}
59
+ className='max-h-[var(--radix-popover-content-available-height,24rem)] w-80 max-w-full overflow-y-auto rounded-xl border border-border bg-popover p-3 text-popover-foreground shadow-lg'
60
+ onKeyDown={(event) => {
61
+ if (event.key === 'Escape' && !event.nativeEvent.isComposing) { event.stopPropagation(); onClose(); }
62
+ }}>
63
+ <div className='flex items-center gap-2'>
64
+ <Settings className='h-4 w-4' />
65
+ <span className='flex-1 text-sm font-medium'>{t('chatInputVoiceShortcut')}</span>
66
+ <IconActionButton icon={<X className='h-4 w-4' />} label={t('chatInputVoiceCloseSettings')} onClick={onClose} />
67
+ </div>
68
+ <ChatVoiceShortcutControl />
69
+ </div>;
70
+ }
@@ -0,0 +1,33 @@
1
+ import { useState } from 'react';
2
+ import { Button } from '@/shared/components/ui/button';
3
+ import { t } from '@/shared/lib/i18n';
4
+ import { isVoiceShortcut, useChatVoiceShortcutStore, voiceShortcutLabel } from '@/features/chat/stores/chat-voice-shortcut.store';
5
+
6
+ export function ChatVoiceShortcutControl() {
7
+ const { shortcut, setShortcut } = useChatVoiceShortcutStore();
8
+ const [capturing, setCapturing] = useState(false);
9
+ const [error, setError] = useState<string | null>(null);
10
+ return <div className='mt-3 border-t border-border/60 pt-3 text-xs text-muted-foreground'>
11
+ <div className='flex flex-wrap items-center gap-2'>
12
+ <Button variant='ghost' size='sm' aria-label={t('chatInputVoiceBind')}
13
+ onClick={() => { setCapturing(true); setError(null); }} onBlur={() => setCapturing(false)}
14
+ onKeyDown={(event) => {
15
+ if (!capturing || event.nativeEvent.isComposing) return;
16
+ if (event.key === 'Escape') { event.stopPropagation(); setCapturing(false); return; }
17
+ if (event.key === 'Tab') return;
18
+ event.preventDefault(); event.stopPropagation();
19
+ const value = { code: event.code, ctrl: event.ctrlKey, meta: event.metaKey, alt: event.altKey, shift: event.shiftKey };
20
+ if (!isVoiceShortcut(value)) { setError('chatInputVoiceBindInvalid'); return; }
21
+ try { setShortcut(value); setCapturing(false); setError(null); }
22
+ catch { setError('chatInputVoiceSaveFailed'); }
23
+ }}>
24
+ {capturing ? t('chatInputVoiceCapture') : shortcut ? voiceShortcutLabel(shortcut) : t('chatInputVoiceBind')}
25
+ </Button>
26
+ {shortcut ? <Button variant='ghost' size='sm' onClick={() => {
27
+ try { setShortcut(null); setError(null); } catch { setError('chatInputVoiceSaveFailed'); }
28
+ }}>{t('chatInputVoiceUnbind')}</Button> : null}
29
+ </div>
30
+ <p role={capturing ? 'status' : undefined} className='mt-1 leading-5'>{t(capturing ? 'chatInputVoiceCaptureHelp' : 'chatInputVoiceShortcutHelp')}</p>
31
+ {error ? <p role='alert' className='mt-1 text-destructive'>{t(error)}</p> : null}
32
+ </div>;
33
+ }
@@ -277,8 +277,6 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
277
277
  setPendingSessionType: inputActions.setPendingSessionType,
278
278
  });
279
279
  const isRuntimeBlocked = isNcpChatRuntimeBlocked(systemStatus);
280
- const currentSessionRunning =
281
- agent.isRunning || inputQuery.selectedSession?.status === "running";
282
280
  const runtimeError = agent.snapshot.error;
283
281
  const rawLastSendError = agent.hydrateError?.message
284
282
  ?? (runtimeError?.code === "run-interrupted" ? null : runtimeError?.message)
@@ -296,16 +294,8 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
296
294
  message: filteredLastSendError,
297
295
  status: systemStatus,
298
296
  });
299
- const controllerAgent = useMemo(
300
- () => ({
301
- ...agent,
302
- isRunning: currentSessionRunning,
303
- isSending: agent.isSending,
304
- }),
305
- [agent, currentSessionRunning],
306
- );
307
297
  const controller = useSessionConversationController({
308
- agent: controllerAgent,
298
+ agent,
309
299
  inputSnapshot,
310
300
  inputQuery,
311
301
  isRuntimeBlocked,
@@ -12,11 +12,15 @@ import {
12
12
  } from '@nextclaw/agent-chat-ui';
13
13
  import { isRuntimeDefaultModelValue } from '@nextclaw/shared';
14
14
 
15
+ import { Mic, Square } from 'lucide-react';
16
+ import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
17
+ import { ChatVoiceInputPanel } from './chat-voice-input-panel';
15
18
  import { useI18n } from '@/app/components/i18n-provider';
16
19
  import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
17
20
  import { type SessionSkillEntryView } from '@/shared/lib/api';
18
21
  import { t } from '@/shared/lib/i18n';
19
22
  import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
23
+ import { useChatVoiceInput } from '@/features/chat/features/conversation/hooks/use-chat-voice-input';
20
24
  import {
21
25
  useChatInputSurfaceState,
22
26
  } from '@/features/chat/features/input/hooks/use-chat-input-surface-state';
@@ -288,6 +292,17 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
288
292
  const persistSessionPreferences = useSessionConversationPreferencePersistence({ inputActions, selectedSessionKey: inputQuery.selectedSessionKey });
289
293
  const handleNodesChange = useSessionConversationComposerNodes(inputActions);
290
294
 
295
+ const voiceManager = presenter.chatVoiceInputManager;
296
+ const voiceContext = JSON.stringify([
297
+ inputQuery.selectedSessionKey, inputQuery.sessionTypeState.selectedSessionType,
298
+ contextReferenceProjectRoot, selectedModel, inputDisabled,
299
+ ]);
300
+ const beginVoiceDraft = useCallback(() => inputBarRef.current?.beginDictation(voiceManager.cancel) ?? null, [voiceManager]);
301
+ const voice = useChatVoiceInput(voiceManager, voiceContext, language, !isMobile && !inputDisabled, beginVoiceDraft);
302
+ const startVoice = () => voiceManager.start(language, voiceContext, beginVoiceDraft());
303
+ const sendWithVoiceCancelled = () => { voiceManager.acceptDraft(); return controller.send(); };
304
+ const steerWithVoiceCancelled = () => { voiceManager.acceptDraft(); return controller.sendSteering(); };
305
+
291
306
  useChatComposerReferenceIntent({
292
307
  inputBarRef,
293
308
  intentManager: presenter.chatComposerIntentManager,
@@ -416,9 +431,9 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
416
431
  <ChatInputBar
417
432
  ref={inputBarRef}
418
433
  surface={useReadingTrack ? 'embedded' : surface}
419
- topSlot={controller.queuedInputs.length > 0
420
- ? <SessionQueuedInputRows controller={controller} />
421
- : null}
434
+ topSlot={controller.queuedInputs.length > 0 ? <SessionQueuedInputRows controller={controller} /> : null}
435
+ floatingSlot={voice.phase !== 'idle' ? <ChatVoiceInputPanel manager={voiceManager} snapshot={voice}
436
+ desktop={!isMobile} onStart={startVoice} /> : null}
422
437
  composer={{
423
438
  nodes: composerNodes,
424
439
  placeholder: textareaPlaceholder,
@@ -441,6 +456,15 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
441
456
  })}
442
457
  toolbar={{
443
458
  addMenuLabel: t('chatInputAdd'),
459
+ leadingSlot: <>
460
+ <IconActionButton icon={voice.phase === 'recording' ? <Square className='h-4 w-4' /> : <Mic className='h-4 w-4' />}
461
+ label={t(voice.phase === 'recording' ? 'chatInputVoiceFinish' : 'chatInputVoice')}
462
+ size='lg'
463
+ disabled={inputDisabled || voice.phase === 'stopping'}
464
+ aria-pressed={voice.phase === 'recording'}
465
+ onClick={() => voice.phase === 'recording' ? voiceManager.finish()
466
+ : voice.phase === 'starting' ? voiceManager.cancel() : startVoice()} />
467
+ </>,
444
468
  selects: [],
445
469
  trailingSelects: toolbarSelects,
446
470
  accessories: [
@@ -465,8 +489,8 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
465
489
  sendIcon: controller.primaryAction,
466
490
  stopButtonLabel: t('chatStop'),
467
491
  contextWindow,
468
- onSend: controller.send,
469
- onAlternateSend: controller.sendSteering,
492
+ onSend: sendWithVoiceCancelled,
493
+ onAlternateSend: steerWithVoiceCancelled,
470
494
  onStop: controller.stop,
471
495
  },
472
496
  }}