@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
@@ -6,6 +6,9 @@ import { ChatConversationParentBanner } from "@/features/chat/components/convers
6
6
  import { ChatConversationSkeleton } from "@/features/chat/components/conversation/chat-conversation-skeleton";
7
7
  import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
8
8
  import { useNcpChatProviderStateResolved } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
9
+ import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
10
+ import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
11
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
9
12
 
10
13
  type ChatConversationLayoutMode = "desktop" | "mobile";
11
14
 
@@ -17,6 +20,9 @@ export function ChatConversationPanel({
17
20
  onBackToList?: () => void;
18
21
  }) {
19
22
  const isProviderStateResolved = useNcpChatProviderStateResolved();
23
+ const showWelcome = useChatThreadStore((state) =>
24
+ shouldShowChatWelcome(state.snapshot),
25
+ );
20
26
 
21
27
  if (!isProviderStateResolved) {
22
28
  return <ChatConversationSkeleton />;
@@ -31,8 +37,15 @@ export function ChatConversationPanel({
31
37
  onBackToList={onBackToList}
32
38
  />
33
39
  <ChatConversationAlerts />
34
- <ChatConversationContent />
35
- <ChatInputBarContainer />
40
+ <ChatConversationContent
41
+ showWelcome={showWelcome}
42
+ welcomeSlot={
43
+ <ChatConversationWelcome
44
+ inputSlot={<ChatInputBarContainer surface="embedded" />}
45
+ />
46
+ }
47
+ />
48
+ {showWelcome ? null : <ChatInputBarContainer />}
36
49
  </div>
37
50
 
38
51
  <ChatConversationWorkspaceSection layoutMode={layoutMode} />
@@ -8,21 +8,12 @@ import { usePresenter } from '@/features/chat/components/providers/chat-presente
8
8
  import { useI18n } from '@/app/components/i18n-provider';
9
9
  import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
10
10
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
11
- import {
12
- useNcpChatProviderStateResolved,
13
- useNcpChatSelectedSession,
14
- } from '@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state';
15
- import { useChatQueryStore } from '@/features/chat/stores/ncp-chat-query.store';
16
- import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
17
- import {
18
- buildNcpChatProviderModelOptions,
19
- filterNcpChatModelOptionsBySessionType,
20
- } from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
21
- import { useChatSessionTypeState } from '@/features/chat/features/session-type/hooks/use-chat-session-type-state';
22
11
  import { chatRecentModelsManager, CHAT_RECENT_MODELS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-models.manager';
23
12
  import { chatRecentSkillsManager, CHAT_RECENT_SKILLS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-skills.manager';
24
13
  import { deriveSelectedSkillsFromComposer } from '@/features/chat/features/input/utils/chat-composer-state.utils';
25
14
  import { hasNcpChatModelOptions, isNcpChatComposerDisabled, isNcpChatModelOptionsEmpty, isNcpChatModelOptionsLoading, isNcpChatSendDisabled } from '@/features/chat/features/input/utils/ncp-chat-input-availability.utils';
15
+ import { useChatInputBarQueryState } from '@/features/chat/features/input/hooks/use-chat-input-bar-query-state';
16
+ import { useChatModelFavorites } from '@/features/chat/features/input/hooks/use-chat-model-favorites';
26
17
  import { useSelectedSessionContextWindowIndicator } from '@/features/chat/features/session/hooks/use-selected-session-context-window-indicator';
27
18
  import { useSystemStatus } from '@/features/system-status';
28
19
  import { isNcpChatRuntimeBlocked } from '@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils';
@@ -30,7 +21,6 @@ import { t } from '@/shared/lib/i18n';
30
21
  import { toast } from 'sonner';
31
22
 
32
23
  type ChatInputStoreSnapshot = ReturnType<typeof useChatInputStore.getState>['snapshot'];
33
- const EMPTY_SESSION_SKILL_RECORDS: SessionSkillEntryView[] = [];
34
24
 
35
25
  function buildThinkingLabels(): Record<ChatThinkingLevel, string> {
36
26
  return {
@@ -100,6 +90,11 @@ function useChatInputBarLabels(language: string) {
100
90
  slashTexts,
101
91
  recentModelsLabel: t('chatPickerRecentModels'),
102
92
  allModelsLabel: t('chatPickerAllModels'),
93
+ favoriteModelsLabel: t('chatPickerFavoriteModels'),
94
+ modelSearchPlaceholder: t('chatModelSearchPlaceholder'),
95
+ modelSearchEmptyLabel: t('chatModelSearchEmpty'),
96
+ favoriteModelLabel: t('chatFavoriteModel'),
97
+ unfavoriteModelLabel: t('chatUnfavoriteModel'),
103
98
  recentSkillsLabel: t('chatPickerRecent'),
104
99
  allSkillsLabel: t('chatPickerAllSkills')
105
100
  };
@@ -173,19 +168,69 @@ function useChatInputBarAttachments(params: { attachmentSupported: boolean; inpu
173
168
  }, [handleFilesAdd])
174
169
  };
175
170
  }
176
- function buildToolbarSelects(params: { allModelsLabel: string; hasModelOptions: boolean; isModelOptionsLoading: boolean; modelRecords: ChatModelRecord[]; onModelChange: (value: string) => void; onThinkingChange: (value: ChatThinkingLevel | null) => void; recentModelValues: string[]; recentModelsLabel: string; selectedModel: string; selectedThinkingLevel: ChatThinkingLevel | null; thinkingSupportedLevels: ChatThinkingLevel[]; thinkingDefaultLevel: ChatThinkingLevel | null; }) {
177
- const { allModelsLabel, hasModelOptions, isModelOptionsLoading, modelRecords, onModelChange, onThinkingChange, recentModelValues, recentModelsLabel, selectedModel, selectedThinkingLevel, thinkingSupportedLevels, thinkingDefaultLevel } = params;
171
+
172
+ type ToolbarSelectBuildParams = {
173
+ allModelsLabel: string;
174
+ favoriteModelLabel: string;
175
+ favoriteModelValues: string[];
176
+ favoriteModelsLabel: string;
177
+ hasModelOptions: boolean;
178
+ isModelOptionsLoading: boolean;
179
+ modelRecords: ChatModelRecord[];
180
+ modelSearchEmptyLabel: string;
181
+ modelSearchPlaceholder: string;
182
+ onFavoriteModelToggle: (value: string, favorite: boolean) => void;
183
+ onModelChange: (value: string) => void;
184
+ onThinkingChange: (value: ChatThinkingLevel | null) => void;
185
+ recentModelValues: string[];
186
+ recentModelsLabel: string;
187
+ selectedModel: string;
188
+ selectedThinkingLevel: ChatThinkingLevel | null;
189
+ thinkingDefaultLevel: ChatThinkingLevel | null;
190
+ thinkingSupportedLevels: ChatThinkingLevel[];
191
+ unfavoriteModelLabel: string;
192
+ };
193
+
194
+ function buildToolbarSelects(params: ToolbarSelectBuildParams) {
195
+ const {
196
+ allModelsLabel,
197
+ favoriteModelLabel,
198
+ favoriteModelValues,
199
+ favoriteModelsLabel,
200
+ hasModelOptions,
201
+ isModelOptionsLoading,
202
+ modelRecords,
203
+ modelSearchEmptyLabel,
204
+ modelSearchPlaceholder,
205
+ onFavoriteModelToggle,
206
+ onModelChange,
207
+ onThinkingChange,
208
+ recentModelValues,
209
+ recentModelsLabel,
210
+ selectedModel,
211
+ selectedThinkingLevel,
212
+ thinkingDefaultLevel,
213
+ thinkingSupportedLevels,
214
+ unfavoriteModelLabel
215
+ } = params;
178
216
  return [
179
217
  buildModelToolbarSelect({
180
218
  modelOptions: modelRecords,
219
+ favoriteModelValues,
181
220
  recentModelValues,
182
221
  selectedModel,
183
222
  isModelOptionsLoading,
184
223
  hasModelOptions,
224
+ onFavoriteToggle: onFavoriteModelToggle,
185
225
  onValueChange: onModelChange,
186
226
  texts: {
187
227
  modelSelectPlaceholder: t('chatSelectModel'),
188
228
  modelNoOptionsLabel: t('chatModelNoOptions'),
229
+ modelSearchPlaceholder,
230
+ modelSearchEmptyLabel,
231
+ favoriteModelsLabel,
232
+ favoriteModelLabel,
233
+ unfavoriteModelLabel,
189
234
  recentModelsLabel,
190
235
  allModelsLabel
191
236
  }
@@ -222,75 +267,11 @@ function buildSkillPicker(params: { allSkillsLabel: string; isSkillsLoading: boo
222
267
  });
223
268
  }
224
269
 
225
- function useChatInputBarQueryState(snapshot: ChatInputStoreSnapshot) {
226
- const presenter = usePresenter();
227
- const selectedSessionKey = useChatSessionListStore(
228
- (state) => state.snapshot.selectedSessionKey,
229
- );
230
- const selectedSession = useNcpChatSelectedSession(selectedSessionKey);
231
- const isProviderStateResolved = useNcpChatProviderStateResolved();
232
- const config = useChatQueryStore(
233
- (state) => state.snapshot.configQuery?.data ?? null,
234
- );
235
- const providersView = useChatQueryStore(
236
- (state) => state.snapshot.providersQuery?.data ?? null,
237
- );
238
- const templatesView = useChatQueryStore(
239
- (state) => state.snapshot.providerTemplatesQuery?.data ?? null,
240
- );
241
- const sessionTypesData = useChatQueryStore(
242
- (state) => state.snapshot.sessionTypesQuery?.data ?? null,
243
- );
244
- const skillRecords = useChatQueryStore(
245
- (state) =>
246
- state.snapshot.sessionSkillsQuery?.data?.records ?? EMPTY_SESSION_SKILL_RECORDS,
247
- );
248
- const isSkillsLoading = useChatQueryStore(
249
- (state) =>
250
- Boolean(
251
- state.snapshot.sessionSkillsQuery?.isLoading ||
252
- state.snapshot.sessionSkillsQuery?.isFetching,
253
- ),
254
- );
255
- const sessionTypeState = useChatSessionTypeState({
256
- selectedSession,
257
- pendingSessionType: snapshot.pendingSessionType,
258
- setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
259
- sessionTypesData,
260
- });
261
- const providerModelOptions = useMemo(
262
- () =>
263
- buildNcpChatProviderModelOptions({
264
- config,
265
- providersView,
266
- templatesView,
267
- }),
268
- [config, providersView, templatesView],
269
- );
270
- const modelOptions = useMemo(
271
- () =>
272
- filterNcpChatModelOptionsBySessionType({
273
- modelOptions: providerModelOptions,
274
- modelSelectionMode: sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
275
- runtimeDefaultModelLabel: t('chatRuntimeDefaultModel'),
276
- supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels,
277
- }),
278
- [
279
- providerModelOptions,
280
- sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
281
- sessionTypeState.selectedSessionTypeOption?.supportedModels,
282
- ],
283
- );
284
-
285
- return {
286
- isProviderStateResolved,
287
- isSkillsLoading,
288
- modelOptions,
289
- skillRecords,
290
- };
291
- }
270
+ type ChatInputBarContainerProps = {
271
+ surface?: 'default' | 'embedded';
272
+ };
292
273
 
293
- export function ChatInputBarContainer() {
274
+ export function ChatInputBarContainer({ surface = 'default' }: ChatInputBarContainerProps) {
294
275
  const presenter = usePresenter();
295
276
  const { language } = useI18n();
296
277
  const { isMobile } = useViewportLayout();
@@ -306,6 +287,14 @@ export function ChatInputBarContainer() {
306
287
  skillRecords: inputQueryState.skillRecords,
307
288
  skillScopeLabels: labels.skillScopeLabels
308
289
  });
290
+ const modelRecordValues = useMemo(
291
+ () => modelRecords.map((option) => option.value),
292
+ [modelRecords]
293
+ );
294
+ const {
295
+ favoriteModelValues,
296
+ setModelFavorite,
297
+ } = useChatModelFavorites(modelRecordValues);
309
298
  const availabilitySnapshot = {
310
299
  ...snapshot,
311
300
  isProviderStateResolved: inputQueryState.isProviderStateResolved,
@@ -330,21 +319,67 @@ export function ChatInputBarContainer() {
330
319
  ? t('chatStopPreparing')
331
320
  : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
332
321
  const { handleFilesAdd, handleFileInputChange } = useChatInputBarAttachments({ attachmentSupported, inputBarRef });
322
+ useEffect(() => {
323
+ presenter.chatInputManager.syncSnapshot({
324
+ isProviderStateResolved: inputQueryState.isProviderStateResolved,
325
+ modelOptions: inputQueryState.modelOptions,
326
+ skillRecords: inputQueryState.skillRecords,
327
+ defaultSessionType: inputQueryState.sessionTypeState.defaultSessionType,
328
+ defaultProjectRoot: inputQueryState.defaultProjectRoot,
329
+ sessionTypeOptions: inputQueryState.sessionTypeState.sessionTypeOptions,
330
+ selectedSessionType: inputQueryState.sessionTypeState.selectedSessionType,
331
+ canEditSessionType: inputQueryState.sessionTypeState.canEditSessionType,
332
+ sessionTypeUnavailable: inputQueryState.sessionTypeState.sessionTypeUnavailable,
333
+ sessionTypeUnavailableMessage: inputQueryState.sessionTypeState.sessionTypeUnavailableMessage,
334
+ });
335
+ presenter.chatInputManager.syncSessionPreferences({
336
+ selectedSessionKey: inputQueryState.selectedSessionKey,
337
+ selectedSessionExists: Boolean(inputQueryState.selectedSession),
338
+ selectedSessionPreferredModel: inputQueryState.selectedSession?.preferredModel,
339
+ selectedSessionPreferredThinking: inputQueryState.selectedSession?.preferredThinking ?? null,
340
+ fallbackPreferredModel: inputQueryState.fallbackPreferredModel,
341
+ fallbackPreferredThinking: inputQueryState.fallbackPreferredThinking,
342
+ defaultModel: inputQueryState.defaultModel,
343
+ });
344
+ }, [
345
+ inputQueryState.defaultModel,
346
+ inputQueryState.defaultProjectRoot,
347
+ inputQueryState.fallbackPreferredModel,
348
+ inputQueryState.fallbackPreferredThinking,
349
+ inputQueryState.isProviderStateResolved,
350
+ inputQueryState.modelOptions,
351
+ inputQueryState.selectedSession,
352
+ inputQueryState.selectedSessionKey,
353
+ inputQueryState.sessionTypeState.canEditSessionType,
354
+ inputQueryState.sessionTypeState.defaultSessionType,
355
+ inputQueryState.sessionTypeState.selectedSessionType,
356
+ inputQueryState.sessionTypeState.sessionTypeOptions,
357
+ inputQueryState.sessionTypeState.sessionTypeUnavailable,
358
+ inputQueryState.sessionTypeState.sessionTypeUnavailableMessage,
359
+ inputQueryState.skillRecords,
360
+ presenter.chatInputManager,
361
+ ]);
333
362
  useEffect(() => {
334
363
  const request = snapshot.composerFocusRequest;
335
364
  if (!request) {
336
365
  return;
337
366
  }
338
367
  if (request.placement === 'end') {
339
- inputBarRef.current?.focusComposerAtEnd();
368
+ inputBarRef.current?.focusComposerAtEnd(snapshot.composerNodes);
340
369
  }
341
370
  presenter.chatInputManager.consumeComposerFocusRequest(request.id);
342
- }, [presenter.chatInputManager, snapshot.composerFocusRequest]);
371
+ }, [presenter.chatInputManager, snapshot.composerFocusRequest, snapshot.composerNodes]);
343
372
  const toolbarSelects = buildToolbarSelects({
344
373
  allModelsLabel: labels.allModelsLabel,
374
+ favoriteModelLabel: labels.favoriteModelLabel,
375
+ favoriteModelValues,
376
+ favoriteModelsLabel: labels.favoriteModelsLabel,
345
377
  hasModelOptions,
346
378
  isModelOptionsLoading,
347
379
  modelRecords,
380
+ modelSearchEmptyLabel: labels.modelSearchEmptyLabel,
381
+ modelSearchPlaceholder: labels.modelSearchPlaceholder,
382
+ onFavoriteModelToggle: setModelFavorite,
348
383
  onModelChange: presenter.chatInputManager.selectModel,
349
384
  onThinkingChange: (value) => {
350
385
  if (value) {
@@ -356,7 +391,8 @@ export function ChatInputBarContainer() {
356
391
  selectedModel: snapshot.selectedModel,
357
392
  selectedThinkingLevel: snapshot.selectedThinkingLevel as ChatThinkingLevel | null,
358
393
  thinkingSupportedLevels,
359
- thinkingDefaultLevel: selectedModelOption?.thinkingCapability?.default ?? null
394
+ thinkingDefaultLevel: selectedModelOption?.thinkingCapability?.default ?? null,
395
+ unfavoriteModelLabel: labels.unfavoriteModelLabel
360
396
  });
361
397
  const skillPicker = buildSkillPicker({
362
398
  allSkillsLabel: labels.allSkillsLabel,
@@ -374,6 +410,7 @@ export function ChatInputBarContainer() {
374
410
  <>
375
411
  <ChatInputBar
376
412
  ref={inputBarRef}
413
+ surface={surface}
377
414
  composer={{
378
415
  nodes: snapshot.composerNodes,
379
416
  placeholder: textareaPlaceholder,
@@ -0,0 +1,120 @@
1
+ import { useMemo } from 'react';
2
+ import type { NcpSessionSummaryView, SessionSkillEntryView } from '@/shared/lib/api';
3
+ import { t } from '@/shared/lib/i18n';
4
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
5
+ import {
6
+ buildNcpChatProviderModelOptions,
7
+ filterNcpChatModelOptionsBySessionType,
8
+ } from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
9
+ import { useNcpChatProviderStateResolved } from '@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state';
10
+ import { useChatSessionTypeState } from '@/features/chat/features/session-type/hooks/use-chat-session-type-state';
11
+ import { adaptNcpSessionSummaries } from '@/features/chat/features/session/utils/ncp-session-adapter.utils';
12
+ import { resolveRecentSessionPreferredValue } from '@/features/chat/features/session/utils/chat-session-preference-governance.utils';
13
+ import { chatRecentModelsManager } from '@/features/chat/managers/chat-recent-models.manager';
14
+ import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
15
+ import { useChatQueryStore } from '@/features/chat/stores/ncp-chat-query.store';
16
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
17
+ import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
18
+
19
+ const EMPTY_SESSION_SKILL_RECORDS: SessionSkillEntryView[] = [];
20
+ const EMPTY_NCP_SESSION_SUMMARIES: NcpSessionSummaryView[] = [];
21
+
22
+ export function useChatInputBarQueryState(snapshot: ChatInputSnapshot) {
23
+ const presenter = usePresenter();
24
+ const selectedSessionKey = useChatSessionListStore(
25
+ (state) => state.snapshot.selectedSessionKey,
26
+ );
27
+ const isProviderStateResolved = useNcpChatProviderStateResolved();
28
+ const querySnapshot = useChatQueryStore((state) => state.snapshot);
29
+ const config = querySnapshot.configQuery?.data ?? null;
30
+ const sessionSummaries =
31
+ querySnapshot.sessionsQuery?.data?.sessions ?? EMPTY_NCP_SESSION_SUMMARIES;
32
+ const skillRecords =
33
+ querySnapshot.sessionSkillsQuery?.data?.records ?? EMPTY_SESSION_SKILL_RECORDS;
34
+ const isSkillsLoading = Boolean(
35
+ querySnapshot.sessionSkillsQuery?.isLoading ||
36
+ querySnapshot.sessionSkillsQuery?.isFetching,
37
+ );
38
+ const sessions = useMemo(
39
+ () => adaptNcpSessionSummaries(sessionSummaries),
40
+ [sessionSummaries],
41
+ );
42
+ const selectedSession = useMemo(
43
+ () => sessions.find((session) => session.key === selectedSessionKey) ?? null,
44
+ [selectedSessionKey, sessions],
45
+ );
46
+ const sessionTypeState = useChatSessionTypeState({
47
+ selectedSession,
48
+ pendingSessionType: snapshot.pendingSessionType,
49
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
50
+ sessionTypesData: querySnapshot.sessionTypesQuery?.data ?? null,
51
+ });
52
+ const providerModelOptions = useMemo(
53
+ () =>
54
+ buildNcpChatProviderModelOptions({
55
+ config,
56
+ providersView: querySnapshot.providersQuery?.data ?? null,
57
+ templatesView: querySnapshot.providerTemplatesQuery?.data ?? null,
58
+ }),
59
+ [config, querySnapshot.providerTemplatesQuery?.data, querySnapshot.providersQuery?.data],
60
+ );
61
+ const modelOptions = useMemo(
62
+ () =>
63
+ filterNcpChatModelOptionsBySessionType({
64
+ modelOptions: providerModelOptions,
65
+ modelSelectionMode: sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
66
+ runtimeDefaultThinkingCapability:
67
+ sessionTypeState.selectedSessionTypeOption?.runtimeDefaultThinking ?? null,
68
+ runtimeDefaultModelLabel: t('chatRuntimeDefaultModel'),
69
+ supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels,
70
+ }),
71
+ [
72
+ providerModelOptions,
73
+ sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
74
+ sessionTypeState.selectedSessionTypeOption?.runtimeDefaultThinking,
75
+ sessionTypeState.selectedSessionTypeOption?.supportedModels,
76
+ ],
77
+ );
78
+ const availableModelValueSet = useMemo(
79
+ () => new Set(modelOptions.map((option) => option.value)),
80
+ [modelOptions],
81
+ );
82
+ const recentSessionTypeModel = chatRecentModelsManager
83
+ .read({ namespace: sessionTypeState.selectedSessionType })
84
+ .find((value) => availableModelValueSet.has(value));
85
+ const fallbackPreferredModel = useMemo(
86
+ () =>
87
+ recentSessionTypeModel ??
88
+ resolveRecentSessionPreferredValue<string>({
89
+ sessions,
90
+ selectedSessionKey,
91
+ sessionType: sessionTypeState.selectedSessionType,
92
+ readPreference: (session) => session.preferredModel?.trim() || undefined,
93
+ }),
94
+ [recentSessionTypeModel, selectedSessionKey, sessionTypeState.selectedSessionType, sessions],
95
+ );
96
+ const fallbackPreferredThinking = useMemo(
97
+ () =>
98
+ resolveRecentSessionPreferredValue({
99
+ sessions,
100
+ selectedSessionKey,
101
+ sessionType: sessionTypeState.selectedSessionType,
102
+ readPreference: (session) => session.preferredThinking ?? undefined,
103
+ }),
104
+ [selectedSessionKey, sessionTypeState.selectedSessionType, sessions],
105
+ );
106
+
107
+ return {
108
+ defaultModel: config?.agents.defaults.model,
109
+ defaultProjectRoot: normalizeSessionProjectRootValue(config?.agents.defaults.workspace),
110
+ fallbackPreferredModel,
111
+ fallbackPreferredThinking,
112
+ isProviderStateResolved,
113
+ isSkillsLoading,
114
+ modelOptions,
115
+ selectedSession,
116
+ selectedSessionKey,
117
+ sessionTypeState,
118
+ skillRecords,
119
+ };
120
+ }
@@ -0,0 +1,87 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
3
+ import {
4
+ fetchPreference,
5
+ updatePreference,
6
+ type PreferenceEntryView,
7
+ type PreferenceJsonValue,
8
+ } from '@/shared/lib/api';
9
+
10
+ const CHAT_MODEL_FAVORITES_PREFERENCE_KEY = 'chat.modelFavorites';
11
+ const chatModelFavoritesQueryKey = ['preference', CHAT_MODEL_FAVORITES_PREFERENCE_KEY] as const;
12
+
13
+ function normalizeFavoriteModelValues(value: PreferenceJsonValue | null | undefined): string[] {
14
+ if (!Array.isArray(value)) {
15
+ return [];
16
+ }
17
+ const values: string[] = [];
18
+ for (const item of value) {
19
+ if (typeof item !== 'string') {
20
+ continue;
21
+ }
22
+ const normalized = item.trim();
23
+ if (normalized && !values.includes(normalized)) {
24
+ values.push(normalized);
25
+ }
26
+ }
27
+ return values;
28
+ }
29
+
30
+ export function useChatModelFavorites(availableModelValues: string[]) {
31
+ const queryClient = useQueryClient();
32
+ const availableModelValueSet = useMemo(
33
+ () => new Set(availableModelValues),
34
+ [availableModelValues],
35
+ );
36
+ const favoritesQuery = useQuery({
37
+ queryKey: chatModelFavoritesQueryKey,
38
+ queryFn: () => fetchPreference(CHAT_MODEL_FAVORITES_PREFERENCE_KEY),
39
+ staleTime: 30_000,
40
+ });
41
+ const favoriteModelValues = useMemo(
42
+ () => normalizeFavoriteModelValues(favoritesQuery.data?.value),
43
+ [favoritesQuery.data?.value],
44
+ );
45
+ const visibleFavoriteModelValues = useMemo(
46
+ () => favoriteModelValues.filter((value) => availableModelValueSet.has(value)),
47
+ [availableModelValueSet, favoriteModelValues],
48
+ );
49
+ const updateFavorites = useMutation({
50
+ mutationFn: async (values: string[]) =>
51
+ await updatePreference(CHAT_MODEL_FAVORITES_PREFERENCE_KEY, values),
52
+ onMutate: async (values) => {
53
+ await queryClient.cancelQueries({ queryKey: chatModelFavoritesQueryKey });
54
+ const previous = queryClient.getQueryData<PreferenceEntryView>(chatModelFavoritesQueryKey);
55
+ queryClient.setQueryData<PreferenceEntryView>(chatModelFavoritesQueryKey, {
56
+ key: CHAT_MODEL_FAVORITES_PREFERENCE_KEY,
57
+ value: values,
58
+ updatedAt: new Date().toISOString(),
59
+ });
60
+ return { previous };
61
+ },
62
+ onError: (_error, _values, context) => {
63
+ if (context?.previous) {
64
+ queryClient.setQueryData(chatModelFavoritesQueryKey, context.previous);
65
+ }
66
+ },
67
+ onSuccess: (entry) => {
68
+ queryClient.setQueryData(chatModelFavoritesQueryKey, entry);
69
+ },
70
+ });
71
+ const setModelFavorite = useCallback((value: string, favorite: boolean) => {
72
+ const normalized = value.trim();
73
+ if (!normalized) {
74
+ return;
75
+ }
76
+ const next = favorite
77
+ ? [normalized, ...favoriteModelValues.filter((item) => item !== normalized)]
78
+ : favoriteModelValues.filter((item) => item !== normalized);
79
+ updateFavorites.mutate(next);
80
+ }, [favoriteModelValues, updateFavorites]);
81
+
82
+ return {
83
+ favoriteModelValues: visibleFavoriteModelValues,
84
+ isLoading: favoritesQuery.isLoading,
85
+ setModelFavorite,
86
+ };
87
+ }