@nextclaw/ui 0.14.0 → 0.14.1-beta.1

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 (194) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/channels-list-page-DfpjB8XB.js +8 -0
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/config-split-page-CeFfbaqc.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/desktop-update-config-CcK6jiEb.js +1 -0
  8. package/dist/assets/{dist-D19wiQwB.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-NZRV-BxD.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/doc-browser-DVHhEb96.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/search-D5FQ5q0X.js +1 -0
  29. package/dist/assets/search-config-page-AMEJJNIS.js +1 -0
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/select-Cu5n0E-3.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.js +1 -0
  35. package/dist/index.html +14 -27
  36. package/package.json +9 -9
  37. package/src/app/presenters/app.presenter.ts +2 -0
  38. package/src/features/account/components/account-panel.tsx +2 -2
  39. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  40. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  41. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  42. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +122 -85
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +120 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +66 -41
  47. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  48. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +22 -36
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -52
  50. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  51. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  52. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  53. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  54. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  55. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  58. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  59. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  60. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +14 -9
  61. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  62. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  63. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  65. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  66. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  67. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  68. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  69. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  70. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  71. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  72. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  73. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  74. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  75. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  76. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  77. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  78. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  79. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  80. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  81. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  82. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  83. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  86. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  87. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  88. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  89. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  90. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  91. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  92. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  93. package/src/features/chat/index.ts +1 -0
  94. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +93 -0
  95. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  96. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  97. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  98. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  99. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  100. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  101. package/src/features/chat/managers/chat-input.manager.ts +37 -15
  102. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  103. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  104. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  105. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  106. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  108. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  109. package/src/features/chat/stores/chat-input.store.ts +9 -1
  110. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  111. package/src/features/chat/types/chat-input-bar.types.ts +5 -1
  112. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  113. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  115. package/src/features/panel-apps/index.ts +2 -0
  116. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  117. package/src/features/settings/components/config/provider-form.tsx +284 -387
  118. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  119. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  120. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  121. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  122. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  123. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  124. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  125. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  126. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  127. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  128. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  129. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  130. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  132. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  133. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  134. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  135. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  136. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  137. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  138. package/src/shared/components/ui/README.md +7 -0
  139. package/src/shared/components/ui/popover.tsx +19 -3
  140. package/src/shared/components/ui/select.tsx +14 -4
  141. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  142. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  143. package/src/shared/lib/api/index.ts +2 -0
  144. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  145. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  146. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  147. package/src/shared/lib/api/types.ts +3 -0
  148. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -1
  149. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  150. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  151. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
  152. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  153. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  154. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  155. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  156. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  157. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  158. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  159. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  160. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  161. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  162. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  163. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  164. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  165. package/dist/assets/index-CMNrRhnG.js +0 -101
  166. package/dist/assets/index-UTmTW1eU.css +0 -1
  167. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  168. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  169. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  170. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  171. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  172. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  173. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  174. package/dist/assets/popover-BnmPOXe3.js +0 -1
  175. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  176. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  177. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  178. package/dist/assets/remote-DNMEy_c4.js +0 -1
  179. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  180. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  181. package/dist/assets/search-D7L-TCH-.js +0 -1
  182. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  183. package/dist/assets/secrets-config-page-95yaH1fa.js +0 -3
  184. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  185. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  186. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  187. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  188. package/dist/assets/use-config-TzwzTgg0.js +0 -1
  189. package/dist/assets/x-fkIWvi07.js +0 -1
  190. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  191. package/src/features/chat/components/chat-welcome.tsx +0 -102
  192. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  193. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  194. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -1,10 +1,9 @@
1
1
  import {
2
2
  buildChatSlashItems,
3
3
  buildModelToolbarSelect,
4
- buildSelectedSkillItems,
5
4
  buildSkillPickerModel
6
- } from '../chat-input-bar.utils';
7
- import type { ChatSkillRecord } from '../chat-input-bar.utils';
5
+ } from '@/features/chat/features/input/utils/chat-input-bar.utils';
6
+ import type { ChatSkillRecord } from '@/features/chat/features/input/utils/chat-input-bar.utils';
8
7
 
9
8
  function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
10
9
  return {
@@ -14,6 +13,20 @@ function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
14
13
  };
15
14
  }
16
15
 
16
+ function createModelTexts() {
17
+ return {
18
+ modelSelectPlaceholder: 'Select model',
19
+ modelNoOptionsLabel: 'No models',
20
+ modelSearchPlaceholder: 'Search models',
21
+ modelSearchEmptyLabel: 'No matching models',
22
+ favoriteModelsLabel: 'Favorites',
23
+ favoriteModelLabel: 'Favorite model',
24
+ unfavoriteModelLabel: 'Remove favorite',
25
+ recentModelsLabel: 'Recent',
26
+ allModelsLabel: 'All models'
27
+ };
28
+ }
29
+
17
30
  describe('buildChatSlashItems', () => {
18
31
  const texts = {
19
32
  slashSkillSubtitle: 'Skill',
@@ -72,20 +85,6 @@ describe('buildChatSlashItems', () => {
72
85
  });
73
86
  });
74
87
 
75
- describe('buildSelectedSkillItems', () => {
76
- it('keeps selected specs and resolves labels when available', () => {
77
- const chips = buildSelectedSkillItems(
78
- ['web-search', 'missing-skill'],
79
- [createSkillRecord({ key: 'web-search', label: 'Web Search' })]
80
- );
81
-
82
- expect(chips).toEqual([
83
- { key: 'web-search', label: 'Web Search' },
84
- { key: 'missing-skill', label: 'missing-skill' }
85
- ]);
86
- });
87
- });
88
-
89
88
  describe('buildSkillPickerModel', () => {
90
89
  it('builds a stable semantic model for toolbar skill picker', () => {
91
90
  const onSelectedKeysChange = vi.fn();
@@ -203,12 +202,7 @@ describe('buildModelToolbarSelect', () => {
203
202
  isModelOptionsLoading: false,
204
203
  hasModelOptions: true,
205
204
  onValueChange,
206
- texts: {
207
- modelSelectPlaceholder: 'Select model',
208
- modelNoOptionsLabel: 'No models',
209
- recentModelsLabel: 'Recent',
210
- allModelsLabel: 'All models'
211
- }
205
+ texts: createModelTexts()
212
206
  });
213
207
 
214
208
  expect(select.value).toBe('minimax/MiniMax-M2.7');
@@ -233,12 +227,7 @@ describe('buildModelToolbarSelect', () => {
233
227
  isModelOptionsLoading: false,
234
228
  hasModelOptions: true,
235
229
  onValueChange: vi.fn(),
236
- texts: {
237
- modelSelectPlaceholder: 'Select model',
238
- modelNoOptionsLabel: 'No models',
239
- recentModelsLabel: 'Recent',
240
- allModelsLabel: 'All models'
241
- }
230
+ texts: createModelTexts()
242
231
  });
243
232
 
244
233
  expect(select.selectedLabel).toBe('Anthropic/claude-sonnet-4-very-long-name');
@@ -269,12 +258,7 @@ describe('buildModelToolbarSelect', () => {
269
258
  isModelOptionsLoading: false,
270
259
  hasModelOptions: true,
271
260
  onValueChange: vi.fn(),
272
- texts: {
273
- modelSelectPlaceholder: 'Select model',
274
- modelNoOptionsLabel: 'No models',
275
- recentModelsLabel: 'Recent',
276
- allModelsLabel: 'All models'
277
- }
261
+ texts: createModelTexts()
278
262
  });
279
263
 
280
264
  expect(select.groups).toEqual([
@@ -305,6 +289,52 @@ describe('buildModelToolbarSelect', () => {
305
289
  ]);
306
290
  });
307
291
 
292
+ it('groups favorite models ahead of recent models without duplicates', () => {
293
+ const onFavoriteToggle = vi.fn();
294
+ const select = buildModelToolbarSelect({
295
+ modelOptions: [
296
+ {
297
+ value: 'openai/gpt-5',
298
+ modelLabel: 'gpt-5',
299
+ providerLabel: 'OpenAI'
300
+ },
301
+ {
302
+ value: 'anthropic/claude-sonnet-4',
303
+ modelLabel: 'claude-sonnet-4',
304
+ providerLabel: 'Anthropic'
305
+ },
306
+ {
307
+ value: 'minimax/MiniMax-M2.7',
308
+ modelLabel: 'MiniMax-M2.7',
309
+ providerLabel: 'MiniMax'
310
+ }
311
+ ],
312
+ favoriteModelValues: ['openai/gpt-5'],
313
+ recentModelValues: ['anthropic/claude-sonnet-4', 'openai/gpt-5'],
314
+ selectedModel: 'openai/gpt-5',
315
+ isModelOptionsLoading: false,
316
+ hasModelOptions: true,
317
+ onFavoriteToggle,
318
+ onValueChange: vi.fn(),
319
+ texts: createModelTexts()
320
+ });
321
+
322
+ expect(select.groups?.map((group) => group.key)).toEqual([
323
+ 'favorite-models',
324
+ 'recent-models',
325
+ 'all-models'
326
+ ]);
327
+ expect(select.groups?.[0]?.options.map((option) => option.value)).toEqual(['openai/gpt-5']);
328
+ expect(select.groups?.[1]?.options.map((option) => option.value)).toEqual(['anthropic/claude-sonnet-4']);
329
+ expect(select.groups?.[2]?.options.map((option) => option.value)).toEqual(['minimax/MiniMax-M2.7']);
330
+ expect(select.optionAction).toMatchObject({
331
+ kind: 'favorite',
332
+ activeValues: ['openai/gpt-5'],
333
+ activeLabel: 'Remove favorite',
334
+ inactiveLabel: 'Favorite model'
335
+ });
336
+ });
337
+
308
338
  it('preserves recent model order from newest to oldest', () => {
309
339
  const select = buildModelToolbarSelect({
310
340
  modelOptions: [
@@ -329,12 +359,7 @@ describe('buildModelToolbarSelect', () => {
329
359
  isModelOptionsLoading: false,
330
360
  hasModelOptions: true,
331
361
  onValueChange: vi.fn(),
332
- texts: {
333
- modelSelectPlaceholder: 'Select model',
334
- modelNoOptionsLabel: 'No models',
335
- recentModelsLabel: 'Recent',
336
- allModelsLabel: 'All models'
337
- }
362
+ texts: createModelTexts()
338
363
  });
339
364
 
340
365
  expect(select.groups?.[0]?.options.map((option) => option.value)).toEqual([
@@ -19,6 +19,7 @@ function createSnapshot(
19
19
  pendingSessionType: 'native',
20
20
  pendingProjectRoot: null,
21
21
  pendingProjectRootSessionKey: null,
22
+ defaultProjectRoot: null,
22
23
  defaultSessionType: 'native',
23
24
  canStopGeneration: false,
24
25
  stopDisabledReason: null,
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ChatSelectedItem,
3
2
  ChatSkillPickerOption,
4
3
  ChatSkillPickerProps,
5
4
  ChatSlashItem,
@@ -7,7 +6,6 @@ import type {
7
6
  export {
8
7
  buildModelStateHint,
9
8
  buildModelToolbarSelect,
10
- buildSessionTypeToolbarSelect,
11
9
  buildThinkingToolbarSelect,
12
10
  } from "./chat-input-toolbar.utils";
13
11
  import type {
@@ -35,14 +33,6 @@ const SLASH_ITEM_MATCH_SCORE = {
35
33
  fallback: 1
36
34
  } as const;
37
35
 
38
- export function resolveSlashQuery(draft: string): string | null {
39
- const match = /^\/([^\s]*)$/.exec(draft);
40
- if (!match) {
41
- return null;
42
- }
43
- return (match[1] ?? '').trim().toLowerCase();
44
- }
45
-
46
36
  function normalizeSearchText(value: string | null | undefined): string {
47
37
  return (value ?? '').trim().toLowerCase();
48
38
  }
@@ -192,20 +182,7 @@ export function buildChatSlashItems(
192
182
  }));
193
183
  }
194
184
 
195
- export function buildSelectedSkillItems(
196
- selectedSkills: string[],
197
- skillRecords: ChatSkillRecord[]
198
- ): ChatSelectedItem[] {
199
- return selectedSkills.map((spec) => {
200
- const matched = skillRecords.find((record) => record.key === spec);
201
- return {
202
- key: spec,
203
- label: matched?.label || spec
204
- };
205
- });
206
- }
207
-
208
- export function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPickerOption[] {
185
+ function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPickerOption[] {
209
186
  return skillRecords.map((record) => ({
210
187
  key: record.key,
211
188
  label: record.label,
@@ -231,8 +208,17 @@ export function buildSkillPickerModel(params: {
231
208
  allSkillsLabel: string;
232
209
  };
233
210
  }): ChatSkillPickerProps {
234
- const prioritizedSkillRecords = prioritizeSkillRecords(params.skillRecords, params.recentSkillValues ?? []);
235
- const recentKeySet = new Set(params.groupedRecentSkillValues ?? []);
211
+ const {
212
+ groupedRecentSkillValues,
213
+ isLoading,
214
+ onSelectedKeysChange,
215
+ recentSkillValues,
216
+ selectedSkills,
217
+ skillRecords,
218
+ texts,
219
+ } = params;
220
+ const prioritizedSkillRecords = prioritizeSkillRecords(skillRecords, recentSkillValues ?? []);
221
+ const recentKeySet = new Set(groupedRecentSkillValues ?? []);
236
222
  const recentSkillOptions = buildSkillPickerOptions(
237
223
  prioritizedSkillRecords.filter((record) => recentKeySet.has(record.key))
238
224
  );
@@ -240,12 +226,12 @@ export function buildSkillPickerModel(params: {
240
226
  prioritizedSkillRecords.filter((record) => !recentKeySet.has(record.key))
241
227
  );
242
228
  return {
243
- title: params.texts.title,
244
- searchPlaceholder: params.texts.searchPlaceholder,
245
- emptyLabel: params.texts.emptyLabel,
246
- loadingLabel: params.texts.loadingLabel,
247
- isLoading: params.isLoading,
248
- manageLabel: params.texts.manageLabel,
229
+ title: texts.title,
230
+ searchPlaceholder: texts.searchPlaceholder,
231
+ emptyLabel: texts.emptyLabel,
232
+ loadingLabel: texts.loadingLabel,
233
+ isLoading,
234
+ manageLabel: texts.manageLabel,
249
235
  manageHref: '/marketplace/skills',
250
236
  options: buildSkillPickerOptions(prioritizedSkillRecords),
251
237
  groups:
@@ -253,17 +239,17 @@ export function buildSkillPickerModel(params: {
253
239
  ? [
254
240
  {
255
241
  key: 'recent-skills',
256
- label: params.texts.recentSkillsLabel,
242
+ label: texts.recentSkillsLabel,
257
243
  options: recentSkillOptions
258
244
  },
259
245
  {
260
246
  key: 'all-skills',
261
- label: params.texts.allSkillsLabel,
247
+ label: texts.allSkillsLabel,
262
248
  options: remainingSkillOptions
263
249
  }
264
250
  ].filter((group) => group.options.length > 0)
265
251
  : undefined,
266
- selectedKeys: params.selectedSkills,
267
- onSelectedKeysChange: params.onSelectedKeysChange
252
+ selectedKeys: selectedSkills,
253
+ onSelectedKeysChange
268
254
  };
269
255
  }
@@ -35,10 +35,16 @@ export function buildModelStateHint(params: {
35
35
  "noModelOptionsLabel" | "configureProviderLabel"
36
36
  >;
37
37
  }): ChatInlineHint | null {
38
- if (!params.isModelOptionsLoading && !params.isModelOptionsEmpty) {
38
+ const {
39
+ isModelOptionsEmpty,
40
+ isModelOptionsLoading,
41
+ onGoToProviders,
42
+ texts,
43
+ } = params;
44
+ if (!isModelOptionsLoading && !isModelOptionsEmpty) {
39
45
  return null;
40
46
  }
41
- if (params.isModelOptionsLoading) {
47
+ if (isModelOptionsLoading) {
42
48
  return {
43
49
  tone: "neutral",
44
50
  loading: true,
@@ -46,25 +52,40 @@ export function buildModelStateHint(params: {
46
52
  }
47
53
  return {
48
54
  tone: "warning",
49
- text: params.texts.noModelOptionsLabel,
50
- actionLabel: params.texts.configureProviderLabel,
51
- onAction: params.onGoToProviders,
55
+ text: texts.noModelOptionsLabel,
56
+ actionLabel: texts.configureProviderLabel,
57
+ onAction: onGoToProviders,
52
58
  };
53
59
  }
54
60
 
55
61
  export function buildModelToolbarSelect({
56
- modelOptions, recentModelValues, selectedModel, isModelOptionsLoading, hasModelOptions, onValueChange, texts,
62
+ modelOptions,
63
+ favoriteModelValues,
64
+ recentModelValues,
65
+ selectedModel,
66
+ isModelOptionsLoading,
67
+ hasModelOptions,
68
+ onFavoriteToggle,
69
+ onValueChange,
70
+ texts,
57
71
  }: {
58
72
  modelOptions: ChatModelRecord[];
73
+ favoriteModelValues?: string[];
59
74
  recentModelValues?: string[];
60
75
  selectedModel: string;
61
76
  isModelOptionsLoading: boolean;
62
77
  hasModelOptions: boolean;
78
+ onFavoriteToggle?: (value: string, favorite: boolean) => void;
63
79
  onValueChange: (value: string) => void;
64
80
  texts: Pick<
65
81
  ChatInputBarAdapterTexts,
66
82
  | "modelSelectPlaceholder"
67
83
  | "modelNoOptionsLabel"
84
+ | "modelSearchPlaceholder"
85
+ | "modelSearchEmptyLabel"
86
+ | "favoriteModelsLabel"
87
+ | "favoriteModelLabel"
88
+ | "unfavoriteModelLabel"
68
89
  | "recentModelsLabel"
69
90
  | "allModelsLabel"
70
91
  >;
@@ -76,19 +97,33 @@ export function buildModelToolbarSelect({
76
97
  const resolvedValue = hasModelOptions
77
98
  ? resolvedModelOption?.value
78
99
  : undefined;
79
- const recentValueSet = new Set(recentModelValues ?? []);
80
100
  const modelOptionMap = new Map(
81
101
  modelOptions.map((option) => [option.value, option] as const),
82
102
  );
83
- const recentOptions = (recentModelValues ?? [])
103
+ const favoriteOptions = (favoriteModelValues ?? [])
84
104
  .map((value) => modelOptionMap.get(value))
85
105
  .filter((option): option is ChatModelRecord => Boolean(option));
106
+ const favoriteValueSet = new Set(favoriteOptions.map((option) => option.value));
107
+ const recentOptions = (recentModelValues ?? [])
108
+ .map((value) => modelOptionMap.get(value))
109
+ .filter(
110
+ (option): option is ChatModelRecord =>
111
+ option !== undefined && !favoriteValueSet.has(option.value),
112
+ );
113
+ const recentValueSet = new Set(recentOptions.map((option) => option.value));
86
114
  const remainingOptions = modelOptions.filter(
87
- (option) => !recentValueSet.has(option.value),
115
+ (option) => !favoriteValueSet.has(option.value) && !recentValueSet.has(option.value),
88
116
  );
89
- const optionGroups =
90
- recentOptions.length > 0
91
- ? [
117
+ const optionGroups = favoriteOptions.length > 0 || recentOptions.length > 0
118
+ ? [
119
+ {
120
+ key: "favorite-models",
121
+ label: texts.favoriteModelsLabel,
122
+ options: favoriteOptions.map((option) => ({
123
+ value: option.value,
124
+ label: formatModelOptionLabel(option),
125
+ })),
126
+ },
92
127
  {
93
128
  key: "recent-models",
94
129
  label: texts.recentModelsLabel,
@@ -106,7 +141,7 @@ export function buildModelToolbarSelect({
106
141
  })),
107
142
  },
108
143
  ].filter((group) => group.options.length > 0)
109
- : undefined;
144
+ : undefined;
110
145
 
111
146
  return {
112
147
  key: "model",
@@ -124,37 +159,23 @@ export function buildModelToolbarSelect({
124
159
  disabled: !hasModelOptions,
125
160
  loading: isModelOptionsLoading,
126
161
  emptyLabel: texts.modelNoOptionsLabel,
162
+ search: {
163
+ placeholder: texts.modelSearchPlaceholder,
164
+ emptyLabel: texts.modelSearchEmptyLabel,
165
+ },
166
+ optionAction: onFavoriteToggle
167
+ ? {
168
+ kind: "favorite",
169
+ activeValues: favoriteOptions.map((option) => option.value),
170
+ activeLabel: texts.unfavoriteModelLabel,
171
+ inactiveLabel: texts.favoriteModelLabel,
172
+ onToggle: onFavoriteToggle,
173
+ }
174
+ : undefined,
127
175
  onValueChange,
128
176
  };
129
177
  }
130
178
 
131
- export function buildSessionTypeToolbarSelect(params: {
132
- selectedSessionType?: string;
133
- selectedSessionTypeOption: { value: string; label: string } | null;
134
- sessionTypeOptions: Array<{ value: string; label: string }>;
135
- onValueChange: (value: string) => void;
136
- canEditSessionType: boolean;
137
- shouldShow: boolean;
138
- texts: Pick<ChatInputBarAdapterTexts, "sessionTypePlaceholder">;
139
- }): ChatToolbarSelect | null {
140
- if (!params.shouldShow) {
141
- return null;
142
- }
143
-
144
- return {
145
- key: "session-type",
146
- value: params.selectedSessionType,
147
- placeholder: params.texts.sessionTypePlaceholder,
148
- selectedLabel: params.selectedSessionTypeOption?.label,
149
- options: params.sessionTypeOptions.map((option) => ({
150
- value: option.value,
151
- label: option.label,
152
- })),
153
- disabled: !params.canEditSessionType,
154
- onValueChange: params.onValueChange,
155
- };
156
- }
157
-
158
179
  export function buildThinkingToolbarSelect(params: {
159
180
  supportedLevels: ChatThinkingLevel[];
160
181
  selectedThinkingLevel: ChatThinkingLevel | null;
@@ -162,31 +183,38 @@ export function buildThinkingToolbarSelect(params: {
162
183
  onValueChange: (value: ChatThinkingLevel) => void;
163
184
  texts: Pick<ChatInputBarAdapterTexts, "thinkingLabels">;
164
185
  }): ChatToolbarSelect | null {
165
- if (params.supportedLevels.length === 0) {
186
+ const {
187
+ defaultThinkingLevel,
188
+ onValueChange,
189
+ selectedThinkingLevel,
190
+ supportedLevels,
191
+ texts,
192
+ } = params;
193
+ if (supportedLevels.length === 0) {
166
194
  return null;
167
195
  }
168
196
 
169
- const options = normalizeThinkingLevels(params.supportedLevels);
197
+ const options = normalizeThinkingLevels(supportedLevels);
170
198
  const fallback = options.includes("off") ? "off" : options[0];
171
199
  const resolvedValue =
172
- (params.selectedThinkingLevel &&
173
- options.includes(params.selectedThinkingLevel) &&
174
- params.selectedThinkingLevel) ||
175
- (params.defaultThinkingLevel &&
176
- options.includes(params.defaultThinkingLevel) &&
177
- params.defaultThinkingLevel) ||
200
+ (selectedThinkingLevel &&
201
+ options.includes(selectedThinkingLevel) &&
202
+ selectedThinkingLevel) ||
203
+ (defaultThinkingLevel &&
204
+ options.includes(defaultThinkingLevel) &&
205
+ defaultThinkingLevel) ||
178
206
  fallback;
179
207
 
180
208
  return {
181
209
  key: "thinking",
182
210
  value: resolvedValue,
183
- placeholder: params.texts.thinkingLabels[resolvedValue],
184
- selectedLabel: params.texts.thinkingLabels[resolvedValue],
211
+ placeholder: texts.thinkingLabels[resolvedValue],
212
+ selectedLabel: texts.thinkingLabels[resolvedValue],
185
213
  icon: "brain",
186
214
  options: options.map((level) => ({
187
215
  value: level,
188
- label: params.texts.thinkingLabels[level],
216
+ label: texts.thinkingLabels[level],
189
217
  })),
190
- onValueChange: (value) => params.onValueChange(value as ChatThinkingLevel),
218
+ onValueChange: (value) => onValueChange(value as ChatThinkingLevel),
191
219
  };
192
220
  }
@@ -0,0 +1,89 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { beforeEach, expect, it, vi } from 'vitest';
3
+ import { ChatInlinePanelAppCard } from '@/features/chat/features/message/components/chat-inline-panel-app-card';
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ handleIframeMessage: vi.fn(),
7
+ open: vi.fn(),
8
+ openTarget: vi.fn(),
9
+ panelApps: {
10
+ data: {
11
+ entries: [
12
+ {
13
+ appId: 'weather-card',
14
+ contentPath: '/api/panel-apps/weather-card/content',
15
+ fileName: 'weather-card.panel.html',
16
+ icon: '*',
17
+ id: 'weather-card',
18
+ title: 'Weather',
19
+ },
20
+ ],
21
+ },
22
+ isLoading: false,
23
+ },
24
+ }));
25
+
26
+ vi.mock('@/app/presenters/app.presenter', () => ({
27
+ getPresenter: () => ({
28
+ panelAppBridgeManager: {
29
+ handleIframeMessage: mocks.handleIframeMessage,
30
+ },
31
+ }),
32
+ }));
33
+
34
+ vi.mock('@/features/panel-apps', () => ({
35
+ findPanelAppEntryByDisplayId: (entries: Array<{ appId: string; id: string; title: string }>, value: string) =>
36
+ entries.find((entry) => [entry.appId, entry.id, entry.title].includes(value)) ?? null,
37
+ usePanelApps: () => mocks.panelApps,
38
+ }));
39
+
40
+ vi.mock('@/shared/components/doc-browser', () => ({
41
+ useDocBrowser: () => ({
42
+ open: mocks.open,
43
+ openTarget: mocks.openTarget,
44
+ }),
45
+ }));
46
+
47
+ vi.mock('@/shared/lib/i18n', () => ({
48
+ t: (key: string) => key,
49
+ }));
50
+
51
+ beforeEach(() => {
52
+ mocks.handleIframeMessage.mockReset();
53
+ mocks.open.mockReset();
54
+ mocks.openTarget.mockReset();
55
+ });
56
+
57
+ it('renders inline panel apps as bounded card-mode iframes with an expand action', () => {
58
+ render(<ChatInlinePanelAppCard panelApp={{
59
+ action: {
60
+ kind: 'show-content',
61
+ label: 'Show content',
62
+ request: {
63
+ placement: 'side_panel',
64
+ target: {
65
+ payload: { appId: 'weather-card' },
66
+ type: 'panel_app',
67
+ },
68
+ title: 'Weather',
69
+ },
70
+ },
71
+ appId: 'weather-card',
72
+ title: 'Weather',
73
+ }} />);
74
+
75
+ const iframe = screen.getByTitle('Weather');
76
+ expect(iframe.getAttribute('src')).toBe(
77
+ '/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline',
78
+ );
79
+ expect(iframe.getAttribute('scrolling')).toBe('auto');
80
+
81
+ fireEvent.click(screen.getByLabelText('chatPanelCardExpand'));
82
+
83
+ expect(mocks.openTarget).toHaveBeenCalledWith(expect.objectContaining({
84
+ dedupeKey: 'panel-app:weather-card',
85
+ kind: 'panel-app',
86
+ title: 'Weather',
87
+ url: '/api/panel-apps/weather-card/content',
88
+ }));
89
+ });
@@ -1,12 +1,13 @@
1
1
  import { render } from "@testing-library/react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import { beforeEach, expect, it, vi } from "vitest";
4
- import { ChatMessageListContainer } from "../chat-message-list.container";
4
+ import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
5
5
 
6
6
  const captures = vi.hoisted(() => ({
7
7
  renders: [] as Array<{
8
8
  messages: unknown[];
9
9
  onToolAction?: (action: unknown) => void;
10
+ renderPanelAppCard?: (panelApp: unknown) => unknown;
10
11
  texts?: Record<string, unknown>;
11
12
  }>,
12
13
  language: "en",
@@ -21,6 +22,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
21
22
  ChatMessageList: (props: {
22
23
  messages: unknown[];
23
24
  onToolAction?: (action: unknown) => void;
25
+ renderPanelAppCard?: (panelApp: unknown) => unknown;
24
26
  texts?: Record<string, unknown>;
25
27
  }) => {
26
28
  captures.renders.push(props);
@@ -251,6 +253,12 @@ it("delegates tool actions to the chat thread manager owner", () => {
251
253
  expect(captures.handleToolAction).toHaveBeenCalledWith(toolAction);
252
254
  });
253
255
 
256
+ it("passes the inline panel app renderer to the shared chat UI", () => {
257
+ render(<ChatMessageListContainer messages={[]} isSending={false} />);
258
+
259
+ expect(captures.renders[captures.renders.length - 1]?.renderPanelAppCard).toEqual(expect.any(Function));
260
+ });
261
+
254
262
  it("renders context compaction as an in-flow divider instead of a chat message", () => {
255
263
  const beforeMessage = {
256
264
  id: "message-before",