@nextclaw/ui 0.12.9 → 0.12.11

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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -1,352 +0,0 @@
1
- import { useMemo, useState } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import { useCreateAgent, useDeleteAgent, useAgents, useUpdateAgent } from '@/hooks/agents/useAgents';
4
- import { useConfig, useConfigMeta } from '@/hooks/useConfig';
5
- import type { AgentProfileView } from '@/api/types';
6
- import {
7
- AgentCreateDialog,
8
- AgentEditDialog,
9
- type AgentCreateFormState,
10
- type AgentEditFormState
11
- } from '@/components/agents/AgentDialogs';
12
- import {
13
- buildSessionTypeOptions,
14
- normalizeSessionType,
15
- resolveAgentRuntimeSessionType,
16
- resolveSessionTypeLabel
17
- } from '@/components/chat/useChatSessionTypeState';
18
- import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
19
- import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
20
- import { AgentAvatar } from '@/components/common/AgentAvatar';
21
- import { Button } from '@/components/ui/button';
22
- import { Card, CardContent } from '@/components/ui/card';
23
- import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
24
- import { PageLayout } from '@/components/layout/page-layout';
25
- import { t } from '@/lib/i18n';
26
- import { buildProviderModelCatalog } from '@/lib/provider-models';
27
- import { cn } from '@/lib/utils';
28
- import { Bot, House, MessageCircle, Pencil, Plus, ShieldCheck, Sparkles, Trash2 } from 'lucide-react';
29
-
30
- const CARD_TONES = [
31
- {
32
- strip: 'bg-[#efc37a]',
33
- chip: 'border-[#f2d7a7] bg-[#fff8eb] text-[#8d5a18]'
34
- },
35
- {
36
- strip: 'bg-[#8fd4c0]',
37
- chip: 'border-[#bde6da] bg-[#effbf7] text-[#156653]'
38
- },
39
- {
40
- strip: 'bg-[#b7c9fb]',
41
- chip: 'border-[#d7e2ff] bg-[#f4f7ff] text-[#2d4d8f]'
42
- }
43
- ] as const;
44
-
45
- function resolveAgentTone(index: number, builtIn: boolean) {
46
- if (builtIn) {
47
- return {
48
- strip: 'bg-[#e6b765]',
49
- chip: 'border-[#f2d19c] bg-[#fff8ec] text-[#90550d]'
50
- };
51
- }
52
- return CARD_TONES[index % CARD_TONES.length];
53
- }
54
-
55
- export function AgentsPage() {
56
- const navigate = useNavigate();
57
- const agentsQuery = useAgents();
58
- const configQuery = useConfig();
59
- const configMetaQuery = useConfigMeta();
60
- const sessionTypesQuery = useNcpChatSessionTypes();
61
- const createAgent = useCreateAgent();
62
- const updateAgent = useUpdateAgent();
63
- const deleteAgent = useDeleteAgent();
64
- const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
65
- const [editingAgent, setEditingAgent] = useState<AgentProfileView | null>(null);
66
- const setSessionListSnapshot = useChatSessionListStore((state) => state.setSnapshot);
67
-
68
- const agents = useMemo(() => agentsQuery.data?.agents ?? [], [agentsQuery.data?.agents]);
69
- const sortedAgents = useMemo(
70
- () =>
71
- [...agents].sort(
72
- (left, right) =>
73
- Number(Boolean(right.builtIn)) - Number(Boolean(left.builtIn)) ||
74
- left.id.localeCompare(right.id)
75
- ),
76
- [agents]
77
- );
78
- const providerCatalog = useMemo(
79
- () => buildProviderModelCatalog({ config: configQuery.data, meta: configMetaQuery.data, onlyConfigured: true }),
80
- [configMetaQuery.data, configQuery.data]
81
- );
82
- const runtimeOptions = useMemo(
83
- () => buildSessionTypeOptions(sessionTypesQuery.data?.options ?? []),
84
- [sessionTypesQuery.data?.options]
85
- );
86
- const defaultRuntime = useMemo(
87
- () => normalizeSessionType(sessionTypesQuery.data?.defaultType ?? 'native'),
88
- [sessionTypesQuery.data?.defaultType]
89
- );
90
- const defaultRuntimeLabel = useMemo(
91
- () => runtimeOptions.find((option) => option.value === defaultRuntime)?.label ?? resolveSessionTypeLabel(defaultRuntime),
92
- [defaultRuntime, runtimeOptions]
93
- );
94
-
95
- const handleCreate = async (form: AgentCreateFormState) => {
96
- await createAgent.mutateAsync({
97
- data: {
98
- id: form.id,
99
- ...(form.displayName.trim() ? { displayName: form.displayName.trim() } : {}),
100
- ...(form.description.trim() ? { description: form.description.trim() } : {}),
101
- ...(form.avatar.trim() ? { avatar: form.avatar.trim() } : {}),
102
- ...(form.home.trim() ? { home: form.home.trim() } : {}),
103
- ...(form.model.trim() ? { model: form.model.trim() } : {}),
104
- ...(form.runtime.trim() ? { runtime: form.runtime.trim() } : {})
105
- }
106
- });
107
- setIsCreateDialogOpen(false);
108
- };
109
-
110
- const handleStartEdit = (agent: AgentProfileView) => {
111
- setEditingAgent(agent);
112
- };
113
-
114
- const handleUpdate = async (agentId: string, form: AgentEditFormState) => {
115
- await updateAgent.mutateAsync({
116
- agentId,
117
- data: {
118
- displayName: form.displayName,
119
- description: form.description,
120
- avatar: form.avatar,
121
- model: form.model,
122
- ...(form.runtime.trim() ? { runtime: form.runtime.trim() } : { runtime: "" })
123
- }
124
- });
125
- setEditingAgent(null);
126
- };
127
-
128
- const startChatWithAgent = (agent: AgentProfileView) => {
129
- setSessionListSnapshot({
130
- selectedAgentId: agent.id,
131
- selectedSessionKey: null
132
- });
133
- useChatInputStore.getState().setSnapshot({
134
- pendingSessionType: resolveAgentRuntimeSessionType(agent, defaultRuntime),
135
- pendingProjectRoot: null,
136
- pendingProjectRootSessionKey: null
137
- });
138
- navigate('/chat');
139
- };
140
-
141
- return (
142
- <PageLayout className="space-y-5">
143
- <section className="relative overflow-hidden rounded-[28px] border border-[#f0d6aa] bg-[linear-gradient(135deg,#fff7ea_0%,#fff9f1_32%,#f2fbff_100%)] px-5 py-5 sm:px-6">
144
- <div className="absolute inset-y-0 right-0 w-[46%] bg-[radial-gradient(circle_at_top_right,rgba(255,215,163,0.52),transparent_54%)]" />
145
- <div className="absolute -bottom-10 left-8 h-32 w-32 rounded-full bg-[#ffe6c0]/55 blur-3xl" />
146
- <div className="relative grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px] xl:items-center">
147
- <div className="max-w-3xl space-y-3">
148
- <div className="inline-flex items-center gap-2 rounded-full border border-white/70 bg-white/80 px-3 py-1 text-[11px] font-semibold tracking-[0.16em] text-[#9b6118]">
149
- <Sparkles className="h-3.5 w-3.5" />
150
- {t('agentsHeroEyebrow')}
151
- </div>
152
- <div className="space-y-2">
153
- <h1 className="max-w-2xl text-[30px] font-semibold leading-tight tracking-[-0.05em] text-[#2f2212] sm:text-[38px]">
154
- {t('agentsHeroTitle')}
155
- </h1>
156
- <p className="max-w-2xl text-sm leading-6 text-[#6d5841] sm:text-[15px] sm:leading-7">
157
- {t('agentsHeroDescription')}
158
- </p>
159
- </div>
160
- <div className="pt-1">
161
- <div className="inline-flex items-center gap-3 rounded-2xl border border-[#f2d5a4] bg-white/82 px-3 py-2 text-[#7a4d12] shadow-[0_14px_30px_rgba(167,117,47,0.07)]">
162
- <span className="text-[11px] font-semibold tracking-[0.14em]">
163
- {t('agentsOverviewTotal')}
164
- </span>
165
- <span className="text-xl font-semibold tracking-[-0.04em] text-[#1f2937]">
166
- {agents.length}
167
- </span>
168
- </div>
169
- </div>
170
- </div>
171
- <div className="flex shrink-0 flex-col gap-3">
172
- <Button
173
- type="button"
174
- className="h-10 rounded-2xl bg-[#1f5c4d] px-5 text-sm font-semibold text-white hover:bg-[#184d40]"
175
- onClick={() => setIsCreateDialogOpen(true)}
176
- >
177
- <Plus className="mr-2 h-4 w-4" />
178
- {t('agentsCreateButton')}
179
- </Button>
180
- <div className="rounded-2xl border border-white/70 bg-white/72 px-4 py-3 text-xs leading-6 text-[#6d5841] shadow-[0_18px_40px_rgba(167,117,47,0.08)]">
181
- {t('agentsCreateDialogHint')}
182
- </div>
183
- </div>
184
- </div>
185
- </section>
186
-
187
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
188
- {agentsQuery.isLoading ? (
189
- <Card className="md:col-span-2 xl:col-span-3 border-dashed border-[#d9dce3] bg-white/70">
190
- <CardContent className="py-14 text-center text-sm text-gray-500">
191
- {t('agentsLoading')}
192
- </CardContent>
193
- </Card>
194
- ) : sortedAgents.length === 0 ? (
195
- <Card className="md:col-span-2 xl:col-span-3 overflow-hidden border-dashed border-[#d9dce3] bg-[linear-gradient(135deg,#fff7ea_0%,#f4fbff_100%)]">
196
- <CardContent className="flex min-h-[240px] flex-col items-center justify-center px-6 py-14 text-center">
197
- <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/80 shadow-[0_18px_44px_rgba(0,0,0,0.08)]">
198
- <Bot className="h-8 w-8 text-[#d39a3b]" />
199
- </div>
200
- <div className="text-lg font-semibold text-[#2f2212]">{t('agentsEmpty')}</div>
201
- <p className="mt-2 max-w-md text-sm leading-6 text-[#78644d]">
202
- {t('agentsEmptyDescription')}
203
- </p>
204
- <Button
205
- type="button"
206
- className="mt-5 rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
207
- onClick={() => setIsCreateDialogOpen(true)}
208
- >
209
- <Plus className="mr-2 h-4 w-4" />
210
- {t('agentsCreateButton')}
211
- </Button>
212
- </CardContent>
213
- </Card>
214
- ) : (
215
- sortedAgents.map((agent, index) => {
216
- const tone = resolveAgentTone(index, Boolean(agent.builtIn));
217
- const runtimeValue = agent.runtime?.trim() || agent.engine?.trim() || '';
218
- const runtimeLabel = runtimeValue
219
- ? runtimeOptions.find((option) => option.value === normalizeSessionType(runtimeValue))?.label ??
220
- resolveSessionTypeLabel(runtimeValue)
221
- : defaultRuntimeLabel;
222
- return (
223
- <Card
224
- key={agent.id}
225
- className="overflow-hidden border border-gray-200 bg-white shadow-sm transition-shadow duration-200 hover:shadow-md"
226
- >
227
- <div className={cn('h-1.5 w-full', tone.strip)} />
228
- <CardContent className="flex h-full flex-col gap-4 px-4 py-4">
229
- <div className="flex items-start gap-3">
230
- <AgentAvatar
231
- agentId={agent.id}
232
- displayName={agent.displayName}
233
- avatarUrl={agent.avatarUrl}
234
- className="h-11 w-11 shrink-0"
235
- />
236
- <div className="min-w-0 flex-1 space-y-1 pt-0.5">
237
- <div className="flex flex-wrap items-center gap-2">
238
- <div className="truncate text-lg font-semibold tracking-[-0.03em] text-[#1f2937]">
239
- {agent.displayName?.trim() || agent.id}
240
- </div>
241
- {agent.builtIn ? (
242
- <span
243
- className={cn(
244
- 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium',
245
- tone.chip
246
- )}
247
- >
248
- <ShieldCheck className="h-3 w-3" />
249
- {t('agentsCardBuiltInTag')}
250
- </span>
251
- ) : null}
252
- </div>
253
- <div className="text-[11px] font-medium uppercase tracking-[0.18em] text-[#94a3b8]">
254
- @{agent.id}
255
- </div>
256
- </div>
257
- </div>
258
-
259
- <p className="text-sm leading-6 text-[#64748b]">
260
- {agent.description?.trim() ||
261
- (agent.builtIn
262
- ? t('agentsCardBuiltInSummary')
263
- : t('agentsCardCustomSummary'))}
264
- </p>
265
-
266
- <div className="mt-auto flex flex-col gap-4">
267
- <div>
268
- <div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
269
- <Sparkles className="h-3.5 w-3.5" />
270
- {t('agentsCardRuntimeLabel')}
271
- </div>
272
- <div className="mt-1.5 text-sm leading-6 text-[#475569]">
273
- {runtimeLabel}
274
- </div>
275
- </div>
276
-
277
- <div className="border-t border-gray-100 pt-3">
278
- <div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
279
- <House className="h-3.5 w-3.5" />
280
- {t('agentsCardHomeLabel')}
281
- </div>
282
- <div className="mt-1.5 break-all text-sm leading-6 text-[#475569]">
283
- {agent.workspace ?? '-'}
284
- </div>
285
- </div>
286
-
287
- <div className="flex flex-wrap items-center gap-2">
288
- <Button
289
- type="button"
290
- className="h-9 rounded-xl bg-[#1f5c4d] px-4 text-white hover:bg-[#184d40]"
291
- onClick={() => startChatWithAgent(agent)}
292
- >
293
- <MessageCircle className="mr-2 h-4 w-4" />
294
- {t('agentsCardStartChat')}
295
- </Button>
296
- <Button
297
- type="button"
298
- variant="ghost"
299
- className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
300
- onClick={() => handleStartEdit(agent)}
301
- disabled={updateAgent.isPending}
302
- >
303
- <Pencil className="mr-1.5 h-3.5 w-3.5" />
304
- {t('agentsEditAction')}
305
- </Button>
306
- {!agent.builtIn ? (
307
- <Button
308
- type="button"
309
- variant="ghost"
310
- className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
311
- onClick={() => deleteAgent.mutate({ agentId: agent.id })}
312
- disabled={deleteAgent.isPending}
313
- >
314
- <Trash2 className="mr-1.5 h-3.5 w-3.5" />
315
- {t('agentsRemoveAction')}
316
- </Button>
317
- ) : null}
318
- </div>
319
- </div>
320
- </CardContent>
321
- </Card>
322
- );
323
- })
324
- )}
325
- </div>
326
-
327
- <AgentCreateDialog
328
- open={isCreateDialogOpen}
329
- pending={createAgent.isPending}
330
- providerCatalog={providerCatalog}
331
- runtimeOptions={runtimeOptions}
332
- defaultRuntime={defaultRuntime}
333
- onOpenChange={setIsCreateDialogOpen}
334
- onSubmit={handleCreate}
335
- />
336
-
337
- <AgentEditDialog
338
- agent={editingAgent}
339
- pending={updateAgent.isPending}
340
- providerCatalog={providerCatalog}
341
- runtimeOptions={runtimeOptions}
342
- defaultRuntime={defaultRuntime}
343
- onOpenChange={(open) => {
344
- if (!open && !updateAgent.isPending) {
345
- setEditingAgent(null);
346
- }
347
- }}
348
- onSubmit={handleUpdate}
349
- />
350
- </PageLayout>
351
- );
352
- }
@@ -1,10 +0,0 @@
1
- export const CONFIG_SPLIT_GRID_CLASS = 'grid min-h-0 grid-cols-1 gap-5 xl:grid-cols-[340px_minmax(0,1fr)]';
2
-
3
- export const CONFIG_SIDEBAR_CARD_CLASS =
4
- 'flex min-h-[520px] min-h-0 min-w-0 flex-col overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]';
5
-
6
- export const CONFIG_DETAIL_CARD_CLASS =
7
- 'flex min-h-[520px] min-h-0 min-w-0 flex-col overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]';
8
-
9
- export const CONFIG_EMPTY_DETAIL_CARD_CLASS =
10
- 'flex min-h-[520px] min-w-0 items-center justify-center overflow-hidden rounded-2xl border border-gray-200/70 bg-white px-6 text-center xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]';
@@ -1,322 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
4
- import type {
5
- MarketplaceInstalledRecord,
6
- MarketplaceInstalledView,
7
- MarketplaceItemSummary,
8
- MarketplaceListView
9
- } from '@/api/types';
10
-
11
- type ItemsQueryState = {
12
- data?: MarketplaceListView;
13
- isLoading: boolean;
14
- isFetching: boolean;
15
- isError: boolean;
16
- error: Error | null;
17
- };
18
-
19
- type InstalledQueryState = {
20
- data?: MarketplaceInstalledView;
21
- isLoading: boolean;
22
- isFetching: boolean;
23
- isError: boolean;
24
- error: Error | null;
25
- };
26
-
27
- const mocks = vi.hoisted(() => ({
28
- navigate: vi.fn(),
29
- docOpen: vi.fn(),
30
- confirm: vi.fn(),
31
- itemsQuery: null as unknown as ItemsQueryState,
32
- installedQuery: null as unknown as InstalledQueryState,
33
- installMutation: {
34
- mutateAsync: vi.fn(),
35
- isPending: false,
36
- variables: undefined
37
- },
38
- manageMutation: {
39
- mutate: vi.fn(),
40
- mutateAsync: vi.fn(),
41
- isPending: false,
42
- variables: undefined
43
- }
44
- }));
45
-
46
- vi.mock('react-router-dom', async () => {
47
- const actual = await vi.importActual('react-router-dom');
48
- return {
49
- ...(actual as object),
50
- useNavigate: () => mocks.navigate,
51
- useParams: () => ({})
52
- };
53
- });
54
-
55
- vi.mock('@/components/doc-browser', () => ({
56
- useDocBrowser: () => ({
57
- open: mocks.docOpen
58
- })
59
- }));
60
-
61
- vi.mock('@/components/providers/I18nProvider', () => ({
62
- useI18n: () => ({
63
- language: 'en'
64
- })
65
- }));
66
-
67
- vi.mock('@/hooks/useConfirmDialog', () => ({
68
- useConfirmDialog: () => ({
69
- confirm: mocks.confirm,
70
- ConfirmDialog: () => null
71
- })
72
- }));
73
-
74
- vi.mock('@/hooks/useMarketplace', () => ({
75
- useMarketplaceItems: () => mocks.itemsQuery,
76
- useMarketplaceInstalled: () => mocks.installedQuery,
77
- useInstallMarketplaceItem: () => mocks.installMutation,
78
- useManageMarketplaceItem: () => mocks.manageMutation
79
- }));
80
-
81
- function createMarketplaceItem(overrides: Partial<MarketplaceItemSummary> = {}): MarketplaceItemSummary {
82
- return {
83
- id: 'skill-web-search',
84
- slug: 'web-search',
85
- type: 'skill',
86
- name: 'Web Search',
87
- summary: 'Search the web from the marketplace',
88
- summaryI18n: { en: 'Search the web from the marketplace' },
89
- tags: ['search'],
90
- author: 'NextClaw',
91
- install: {
92
- kind: 'marketplace',
93
- spec: '@nextclaw/web-search',
94
- command: 'nextclaw skills install @nextclaw/web-search'
95
- },
96
- updatedAt: '2026-03-17T00:00:00.000Z',
97
- ...overrides
98
- };
99
- }
100
-
101
- function createPluginMarketplaceItem(overrides: Partial<MarketplaceItemSummary> = {}): MarketplaceItemSummary {
102
- return createMarketplaceItem({
103
- id: 'plugin-codex-runtime',
104
- slug: 'codex-runtime',
105
- type: 'plugin',
106
- name: 'Codex SDK NCP Runtime',
107
- summary: 'Optional Codex runtime for NextClaw',
108
- summaryI18n: { en: 'Optional Codex runtime for NextClaw' },
109
- install: {
110
- kind: 'npm',
111
- spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
112
- command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'
113
- },
114
- ...overrides
115
- });
116
- }
117
-
118
- function createInstalledRecord(overrides: Partial<MarketplaceInstalledRecord> = {}): MarketplaceInstalledRecord {
119
- return {
120
- type: 'plugin',
121
- id: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
122
- spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
123
- label: 'Codex SDK NCP Runtime',
124
- enabled: true,
125
- origin: 'marketplace',
126
- source: 'marketplace',
127
- installedAt: '2026-03-19T00:00:00.000Z',
128
- ...overrides
129
- };
130
- }
131
-
132
- function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
133
- return {
134
- data: undefined as MarketplaceListView | undefined,
135
- isLoading: false,
136
- isFetching: false,
137
- isError: false,
138
- error: null,
139
- ...overrides
140
- };
141
- }
142
-
143
- function createInstalledQuery(overrides: Partial<Record<string, unknown>> = {}) {
144
- return {
145
- data: {
146
- type: 'skill',
147
- total: 0,
148
- specs: [],
149
- records: []
150
- } satisfies MarketplaceInstalledView,
151
- isLoading: false,
152
- isFetching: false,
153
- isError: false,
154
- error: null,
155
- ...overrides
156
- };
157
- }
158
-
159
- describe('MarketplacePage', () => {
160
- beforeEach(() => {
161
- mocks.navigate.mockReset();
162
- mocks.docOpen.mockReset();
163
- mocks.confirm.mockReset();
164
- mocks.installMutation.mutateAsync.mockReset();
165
- mocks.manageMutation.mutate.mockReset();
166
- mocks.manageMutation.mutateAsync.mockReset();
167
- mocks.installMutation.isPending = false;
168
- mocks.installMutation.variables = undefined;
169
- mocks.manageMutation.isPending = false;
170
- mocks.manageMutation.variables = undefined;
171
- mocks.itemsQuery = createItemsQuery();
172
- mocks.installedQuery = createInstalledQuery();
173
- });
174
-
175
- it('renders skeleton cards during initial skills loading', () => {
176
- mocks.itemsQuery = createItemsQuery({
177
- isLoading: true,
178
- isFetching: true
179
- });
180
-
181
- const { container } = render(<MarketplacePage forcedType="skills" />);
182
-
183
- expect(screen.getByTestId('marketplace-list-skeleton')).toBeTruthy();
184
- expect(container.querySelectorAll('[data-testid="marketplace-list-skeleton"] > article')).toHaveLength(12);
185
- });
186
-
187
- it('keeps loaded cards visible during background refresh', () => {
188
- mocks.itemsQuery = createItemsQuery({
189
- data: {
190
- total: 1,
191
- page: 1,
192
- pageSize: 12,
193
- totalPages: 1,
194
- sort: 'relevance',
195
- items: [createMarketplaceItem()]
196
- } satisfies MarketplaceListView,
197
- isFetching: true
198
- });
199
-
200
- render(<MarketplacePage forcedType="skills" />);
201
-
202
- expect(screen.queryByTestId('marketplace-list-skeleton')).toBeNull();
203
- expect(screen.getByText('Web Search')).toBeTruthy();
204
- });
205
-
206
- it('does not render the redundant plugin type label in plugin cards', () => {
207
- mocks.itemsQuery = createItemsQuery({
208
- data: {
209
- total: 1,
210
- page: 1,
211
- pageSize: 12,
212
- totalPages: 1,
213
- sort: 'relevance',
214
- items: [
215
- createMarketplaceItem({
216
- id: 'plugin-codex-runtime',
217
- slug: 'codex-runtime',
218
- type: 'plugin',
219
- name: 'Codex SDK NCP Runtime',
220
- summary: 'Optional Codex runtime for NextClaw',
221
- summaryI18n: { en: 'Optional Codex runtime for NextClaw' },
222
- install: {
223
- kind: 'npm',
224
- spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
225
- command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'
226
- }
227
- })
228
- ]
229
- } satisfies MarketplaceListView
230
- });
231
-
232
- const { container } = render(<MarketplacePage forcedType="plugins" />);
233
- const card = container.querySelector('article');
234
-
235
- expect(card?.textContent).toContain('@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk');
236
- expect(card?.textContent).not.toContain('Plugin');
237
- });
238
-
239
- it('does not dim the loaded list during background refresh', () => {
240
- mocks.itemsQuery = createItemsQuery({
241
- data: {
242
- total: 1,
243
- page: 1,
244
- pageSize: 12,
245
- totalPages: 1,
246
- sort: 'relevance',
247
- items: [createPluginMarketplaceItem()]
248
- } satisfies MarketplaceListView,
249
- isFetching: true
250
- });
251
-
252
- const { container } = render(<MarketplacePage forcedType="plugins" />);
253
-
254
- expect(screen.getByText('Codex SDK NCP Runtime')).toBeTruthy();
255
- expect(container.querySelector('.opacity-70')).toBeNull();
256
- });
257
-
258
- it('only disables the targeted plugin action while a manage request is pending', async () => {
259
- const user = userEvent.setup();
260
- let resolveMutation: (() => void) | undefined;
261
- mocks.itemsQuery = createItemsQuery({
262
- data: {
263
- total: 2,
264
- page: 1,
265
- pageSize: 12,
266
- totalPages: 1,
267
- sort: 'relevance',
268
- items: [
269
- createPluginMarketplaceItem(),
270
- createPluginMarketplaceItem({
271
- id: 'plugin-claude-runtime',
272
- slug: 'claude-runtime',
273
- name: 'Claude Agent Runtime',
274
- install: {
275
- kind: 'npm',
276
- spec: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
277
- command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk'
278
- }
279
- })
280
- ]
281
- } satisfies MarketplaceListView
282
- });
283
- mocks.installedQuery = createInstalledQuery({
284
- data: {
285
- type: 'plugin',
286
- total: 2,
287
- specs: [
288
- '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
289
- '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk'
290
- ],
291
- records: [
292
- createInstalledRecord(),
293
- createInstalledRecord({
294
- id: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
295
- spec: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
296
- label: 'Claude Agent Runtime'
297
- })
298
- ]
299
- } satisfies MarketplaceInstalledView
300
- });
301
- mocks.manageMutation.mutateAsync.mockImplementation(
302
- () => new Promise<void>((resolve) => {
303
- resolveMutation = resolve;
304
- })
305
- );
306
-
307
- render(<MarketplacePage forcedType="plugins" />);
308
-
309
- const disableButtons = screen.getAllByRole('button', { name: 'Disable' });
310
- const firstDisableButton = disableButtons[0];
311
- const secondDisableButton = disableButtons[1];
312
-
313
- await user.click(firstDisableButton);
314
-
315
- expect(mocks.manageMutation.mutateAsync).toHaveBeenCalledTimes(1);
316
- expect(firstDisableButton.hasAttribute('disabled')).toBe(true);
317
- expect(firstDisableButton.textContent).toContain('Disabling');
318
- expect(secondDisableButton.hasAttribute('disabled')).toBe(false);
319
-
320
- resolveMutation?.();
321
- });
322
- });