@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -0,0 +1,128 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ } from "@/api/types";
5
+ import { Button } from "@/components/ui/button";
6
+ import { TagChip } from "@/components/ui/tag-chip";
7
+ import { t } from "@/lib/i18n";
8
+ import { readSummary, readTransportLabel } from "./mcp-marketplace-doc";
9
+
10
+ export function McpMarketplaceCard(props: {
11
+ item?: MarketplaceItemSummary;
12
+ record?: MarketplaceInstalledRecord;
13
+ localeFallbacks: string[];
14
+ onOpen: () => void;
15
+ onInstall?: () => void;
16
+ onToggle?: () => void;
17
+ onDoctor?: () => void;
18
+ onRemove?: () => void;
19
+ }) {
20
+ const {
21
+ item,
22
+ record,
23
+ localeFallbacks,
24
+ onOpen,
25
+ onInstall,
26
+ onToggle,
27
+ onDoctor,
28
+ onRemove,
29
+ } = props;
30
+ const installed = record;
31
+ const name = item?.name ?? record?.label ?? record?.id ?? "MCP";
32
+ const summary = readSummary(localeFallbacks, item, record);
33
+ const transport = readTransportLabel(item, record);
34
+ const status = installed
35
+ ? installed.enabled === false
36
+ ? t("marketplaceDisable")
37
+ : t("statusReady")
38
+ : null;
39
+
40
+ return (
41
+ <article
42
+ onClick={onOpen}
43
+ className="cursor-pointer rounded-2xl border border-gray-200/70 bg-white p-4 shadow-sm transition hover:border-blue-300 hover:shadow-md"
44
+ >
45
+ <div className="flex items-start justify-between gap-3">
46
+ <div className="min-w-0">
47
+ <div className="text-sm font-semibold text-gray-900">{name}</div>
48
+ <div className="mt-1 text-xs text-gray-500">{transport}</div>
49
+ <div className="mt-2 line-clamp-2 text-sm text-gray-600">
50
+ {summary}
51
+ </div>
52
+ <div className="mt-3 flex flex-wrap gap-2">
53
+ {(item?.tags ?? []).map((tag) => (
54
+ <TagChip key={tag}>{tag}</TagChip>
55
+ ))}
56
+ {status ? <TagChip tone="success">{status}</TagChip> : null}
57
+ </div>
58
+ </div>
59
+
60
+ <div className="flex shrink-0 flex-col gap-2">
61
+ {!installed && item && onInstall ? (
62
+ <Button
63
+ type="button"
64
+ size="sm"
65
+ variant="primary"
66
+ className="rounded-xl"
67
+ onClick={(event) => {
68
+ event.stopPropagation();
69
+ onInstall();
70
+ }}
71
+ >
72
+ {t("marketplaceInstall")}
73
+ </Button>
74
+ ) : null}
75
+
76
+ {installed ? (
77
+ <>
78
+ {onToggle ? (
79
+ <Button
80
+ type="button"
81
+ size="sm"
82
+ variant="outline"
83
+ className="rounded-xl border-gray-200 text-gray-700"
84
+ onClick={(event) => {
85
+ event.stopPropagation();
86
+ onToggle();
87
+ }}
88
+ >
89
+ {installed.enabled === false
90
+ ? t("marketplaceEnable")
91
+ : t("marketplaceDisable")}
92
+ </Button>
93
+ ) : null}
94
+ {onDoctor ? (
95
+ <Button
96
+ type="button"
97
+ size="sm"
98
+ variant="outline"
99
+ className="rounded-xl border-blue-200 text-blue-700 hover:border-blue-300 hover:bg-blue-50 hover:text-blue-700"
100
+ onClick={(event) => {
101
+ event.stopPropagation();
102
+ onDoctor();
103
+ }}
104
+ >
105
+ {t("marketplaceMcpDoctor")}
106
+ </Button>
107
+ ) : null}
108
+ {onRemove ? (
109
+ <Button
110
+ type="button"
111
+ size="sm"
112
+ variant="outline"
113
+ className="rounded-xl border-rose-200 text-rose-600 hover:border-rose-300 hover:bg-rose-50 hover:text-rose-600"
114
+ onClick={(event) => {
115
+ event.stopPropagation();
116
+ onRemove();
117
+ }}
118
+ >
119
+ {t("marketplaceMcpRemove")}
120
+ </Button>
121
+ ) : null}
122
+ </>
123
+ ) : null}
124
+ </div>
125
+ </div>
126
+ </article>
127
+ );
128
+ }
@@ -0,0 +1,191 @@
1
+ import { useState } from "react";
2
+ import type {
3
+ MarketplaceItemSummary,
4
+ MarketplaceMcpDoctorResult,
5
+ MarketplaceMcpInstallSpec,
6
+ } from "@/api/types";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from "@/components/ui/dialog";
16
+ import { Input } from "@/components/ui/input";
17
+ import { NoticeCard } from "@/components/ui/notice-card";
18
+ import { Switch } from "@/components/ui/switch";
19
+ import { t } from "@/lib/i18n";
20
+
21
+ export function InstallDialog(props: {
22
+ item: MarketplaceItemSummary | null;
23
+ open: boolean;
24
+ pending: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ onSubmit: (payload: {
27
+ name: string;
28
+ allAgents: boolean;
29
+ inputs: Record<string, string>;
30
+ }) => Promise<void>;
31
+ }) {
32
+ const { item, open, pending, onOpenChange, onSubmit } = props;
33
+
34
+ return (
35
+ <Dialog open={open} onOpenChange={onOpenChange}>
36
+ {item ? (
37
+ <InstallDialogContent
38
+ key={`${item.slug}:${open ? "open" : "closed"}`}
39
+ item={item}
40
+ pending={pending}
41
+ onOpenChange={onOpenChange}
42
+ onSubmit={onSubmit}
43
+ />
44
+ ) : null}
45
+ </Dialog>
46
+ );
47
+ }
48
+
49
+ function InstallDialogContent(props: {
50
+ item: MarketplaceItemSummary;
51
+ pending: boolean;
52
+ onOpenChange: (open: boolean) => void;
53
+ onSubmit: (payload: {
54
+ name: string;
55
+ allAgents: boolean;
56
+ inputs: Record<string, string>;
57
+ }) => Promise<void>;
58
+ }) {
59
+ const { item, pending, onOpenChange, onSubmit } = props;
60
+ const template = item.install as MarketplaceMcpInstallSpec | undefined;
61
+ const [name, setName] = useState(template?.defaultName ?? "");
62
+ const [allAgents, setAllAgents] = useState(true);
63
+ const [inputs, setInputs] = useState<Record<string, string>>(
64
+ Object.fromEntries(
65
+ (template?.inputs ?? []).map((field) => [
66
+ field.id,
67
+ field.defaultValue ?? "",
68
+ ]),
69
+ ),
70
+ );
71
+
72
+ return (
73
+ <DialogContent>
74
+ <DialogHeader>
75
+ <DialogTitle>{t("marketplaceMcpInstallDialogTitle")}</DialogTitle>
76
+ <DialogDescription>{item.name}</DialogDescription>
77
+ </DialogHeader>
78
+
79
+ <div className="space-y-4">
80
+ <div className="space-y-2">
81
+ <div className="text-sm font-medium text-gray-800">
82
+ {t("marketplaceMcpServerName")}
83
+ </div>
84
+ <Input
85
+ value={name}
86
+ onChange={(event) => setName(event.target.value)}
87
+ placeholder={template?.defaultName ?? "mcp-server"}
88
+ />
89
+ </div>
90
+
91
+ <div className="flex items-center justify-between rounded-xl border border-gray-200 px-3 py-3">
92
+ <div>
93
+ <div className="text-sm font-medium text-gray-900">
94
+ {t("marketplaceMcpAllAgents")}
95
+ </div>
96
+ <div className="text-xs text-gray-500">
97
+ {t("marketplaceMcpAllAgentsDescription")}
98
+ </div>
99
+ </div>
100
+ <Switch checked={allAgents} onCheckedChange={setAllAgents} />
101
+ </div>
102
+
103
+ {(template?.inputs ?? []).map((field) => (
104
+ <div key={field.id} className="space-y-2">
105
+ <div className="text-sm font-medium text-gray-800">
106
+ {field.label}
107
+ </div>
108
+ {field.description ? (
109
+ <div className="text-xs text-gray-500">{field.description}</div>
110
+ ) : null}
111
+ <Input
112
+ type={field.secret ? "password" : "text"}
113
+ value={inputs[field.id] ?? ""}
114
+ onChange={(event) =>
115
+ setInputs((current) => ({
116
+ ...current,
117
+ [field.id]: event.target.value,
118
+ }))
119
+ }
120
+ placeholder={field.defaultValue ?? ""}
121
+ />
122
+ </div>
123
+ ))}
124
+ </div>
125
+
126
+ <DialogFooter>
127
+ <Button
128
+ variant="outline"
129
+ onClick={() => onOpenChange(false)}
130
+ disabled={pending}
131
+ >
132
+ {t("cancel")}
133
+ </Button>
134
+ <Button
135
+ onClick={() => void onSubmit({ name, allAgents, inputs })}
136
+ disabled={pending || !name.trim()}
137
+ >
138
+ {pending ? t("marketplaceInstalling") : t("marketplaceInstall")}
139
+ </Button>
140
+ </DialogFooter>
141
+ </DialogContent>
142
+ );
143
+ }
144
+
145
+ export function DoctorDialog(props: {
146
+ result: MarketplaceMcpDoctorResult | null;
147
+ targetName: string | null;
148
+ open: boolean;
149
+ pending: boolean;
150
+ onOpenChange: (open: boolean) => void;
151
+ }) {
152
+ const { result, targetName, open, pending, onOpenChange } = props;
153
+
154
+ return (
155
+ <Dialog open={open} onOpenChange={onOpenChange}>
156
+ <DialogContent>
157
+ <DialogHeader>
158
+ <DialogTitle>{t("marketplaceMcpDoctorTitle")}</DialogTitle>
159
+ <DialogDescription>{targetName ?? "-"}</DialogDescription>
160
+ </DialogHeader>
161
+ {pending ? (
162
+ <div className="text-sm text-gray-500">{t("loading")}</div>
163
+ ) : null}
164
+ {!pending && result ? (
165
+ <div className="space-y-3 text-sm text-gray-700">
166
+ <div>
167
+ {t("marketplaceMcpDoctorAccessible")}:{" "}
168
+ {result.accessible
169
+ ? t("statusReady")
170
+ : t("marketplaceOperationFailed")}
171
+ </div>
172
+ <div>
173
+ {t("marketplaceMcpDoctorTransport")}:{" "}
174
+ {result.transport.toUpperCase()}
175
+ </div>
176
+ <div>
177
+ {t("marketplaceMcpDoctorTools")}: {result.toolCount}
178
+ </div>
179
+ {result.error ? (
180
+ <NoticeCard
181
+ tone="danger"
182
+ description={result.error}
183
+ className="rounded-lg"
184
+ />
185
+ ) : null}
186
+ </div>
187
+ ) : null}
188
+ </DialogContent>
189
+ </Dialog>
190
+ );
191
+ }
@@ -0,0 +1,152 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ MarketplaceMcpInstallSpec,
5
+ } from "@/api/types";
6
+ import {
7
+ pickInstalledRecordDescription,
8
+ pickLocalizedText,
9
+ } from "@/components/marketplace/marketplace-localization";
10
+ import { t } from "@/lib/i18n";
11
+
12
+ function normalizeMarketplaceKey(value: string | undefined): string {
13
+ return (value ?? "").trim().toLowerCase();
14
+ }
15
+
16
+ export function buildInstalledRecordLookup(
17
+ records: MarketplaceInstalledRecord[],
18
+ ): Map<string, MarketplaceInstalledRecord> {
19
+ const lookup = new Map<string, MarketplaceInstalledRecord>();
20
+
21
+ for (const record of records) {
22
+ const candidates = [
23
+ record.catalogSlug,
24
+ record.spec,
25
+ record.id,
26
+ record.label,
27
+ ];
28
+ for (const candidate of candidates) {
29
+ const normalized = normalizeMarketplaceKey(candidate);
30
+ if (!normalized || lookup.has(normalized)) {
31
+ continue;
32
+ }
33
+ lookup.set(normalized, record);
34
+ }
35
+ }
36
+
37
+ return lookup;
38
+ }
39
+
40
+ export function findInstalledRecordForItem(
41
+ item: MarketplaceItemSummary,
42
+ installedRecordLookup: Map<string, MarketplaceInstalledRecord>,
43
+ ): MarketplaceInstalledRecord | undefined {
44
+ const candidates = [item.slug, item.install.spec, item.id, item.name];
45
+ for (const candidate of candidates) {
46
+ const normalized = normalizeMarketplaceKey(candidate);
47
+ if (!normalized) {
48
+ continue;
49
+ }
50
+ const record = installedRecordLookup.get(normalized);
51
+ if (record) {
52
+ return record;
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function escape(value: string) {
59
+ return value
60
+ .replace(/&/g, "&amp;")
61
+ .replace(/</g, "&lt;")
62
+ .replace(/>/g, "&gt;")
63
+ .replace(/"/g, "&quot;")
64
+ .replace(/'/g, "&#39;");
65
+ }
66
+
67
+ export function buildDocDataUrl(
68
+ title: string,
69
+ metadata: string,
70
+ content: string,
71
+ sourceUrl?: string,
72
+ summary?: string,
73
+ ): string {
74
+ const html = `<!doctype html>
75
+ <html>
76
+ <head>
77
+ <meta charset="utf-8" />
78
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
79
+ <title>${escape(title)}</title>
80
+ <style>
81
+ body { margin: 0; background: #f8fafc; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
82
+ .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 40px; }
83
+ .hero { border: 1px solid #dbeafe; border-radius: 16px; background: linear-gradient(180deg, #eff6ff, #ffffff); padding: 20px; }
84
+ .hero h1 { margin: 0; font-size: 26px; }
85
+ .grid { display: grid; grid-template-columns: 280px 1fr; gap: 14px; margin-top: 16px; }
86
+ .card { border: 1px solid #e2e8f0; background: #fff; border-radius: 14px; overflow: hidden; }
87
+ .card h2 { margin: 0; padding: 12px 14px; font-size: 13px; font-weight: 700; color: #1d4ed8; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
88
+ .body { padding: 12px 14px; }
89
+ pre { margin: 0; white-space: pre-wrap; line-height: 1.7; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
90
+ a { color: #2563eb; text-decoration: none; }
91
+ @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <main class="wrap">
96
+ <section class="hero">
97
+ <h1>${escape(title)}</h1>
98
+ ${summary ? `<p>${escape(summary)}</p>` : ""}
99
+ ${sourceUrl ? `<p><a href="${escape(sourceUrl)}" target="_blank" rel="noopener noreferrer">${escape(sourceUrl)}</a></p>` : ""}
100
+ </section>
101
+ <section class="grid">
102
+ <article class="card">
103
+ <h2>Metadata</h2>
104
+ <div class="body"><pre>${escape(metadata)}</pre></div>
105
+ </article>
106
+ <article class="card">
107
+ <h2>Content</h2>
108
+ <div class="body"><pre>${escape(content)}</pre></div>
109
+ </article>
110
+ </section>
111
+ </main>
112
+ </body>
113
+ </html>`;
114
+
115
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
116
+ }
117
+
118
+ export function readSummary(
119
+ localeFallbacks: string[],
120
+ item?: MarketplaceItemSummary,
121
+ record?: MarketplaceInstalledRecord,
122
+ ): string {
123
+ const localizedSummary = pickLocalizedText(
124
+ item?.summaryI18n,
125
+ item?.summary,
126
+ localeFallbacks,
127
+ );
128
+ if (localizedSummary) {
129
+ return localizedSummary;
130
+ }
131
+
132
+ const localizedRecordDescription = pickInstalledRecordDescription(
133
+ record,
134
+ localeFallbacks,
135
+ );
136
+ return localizedRecordDescription || t("marketplaceInstalledLocalSummary");
137
+ }
138
+
139
+ export function readTransportLabel(
140
+ item?: MarketplaceItemSummary,
141
+ record?: MarketplaceInstalledRecord,
142
+ ): string {
143
+ if (record?.transport) {
144
+ return record.transport.toUpperCase();
145
+ }
146
+ const install = item?.install as MarketplaceMcpInstallSpec | undefined;
147
+ return (
148
+ (install?.transportTypes ?? [])
149
+ .map((entry) => entry.toUpperCase())
150
+ .join(" / ") || "MCP"
151
+ );
152
+ }
@@ -0,0 +1,223 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, beforeEach, expect, it, vi } from "vitest";
3
+ import { McpMarketplacePage } from "@/components/marketplace/mcp/mcp-marketplace-page";
4
+ import type {
5
+ MarketplaceInstalledView,
6
+ MarketplaceListView,
7
+ } from "@/api/types";
8
+
9
+ type ItemsQueryState = {
10
+ data?: MarketplaceListView;
11
+ isLoading: boolean;
12
+ isFetching: boolean;
13
+ isError: boolean;
14
+ error: Error | null;
15
+ };
16
+
17
+ type InstalledQueryState = {
18
+ data?: MarketplaceInstalledView;
19
+ isLoading: boolean;
20
+ isFetching: boolean;
21
+ isError: boolean;
22
+ error: Error | null;
23
+ };
24
+
25
+ const mocks = vi.hoisted(() => ({
26
+ itemsQuery: null as unknown as ItemsQueryState,
27
+ installedQuery: null as unknown as InstalledQueryState,
28
+ installMutation: {
29
+ mutateAsync: vi.fn(),
30
+ isPending: false,
31
+ },
32
+ manageMutation: {
33
+ mutateAsync: vi.fn(),
34
+ isPending: false,
35
+ },
36
+ doctorMutation: {
37
+ mutateAsync: vi.fn(),
38
+ isPending: false,
39
+ },
40
+ confirm: vi.fn(),
41
+ docOpen: vi.fn(),
42
+ }));
43
+
44
+ vi.mock("@tanstack/react-query", () => ({
45
+ useMutation: () => mocks.doctorMutation,
46
+ }));
47
+
48
+ vi.mock("@/components/providers/I18nProvider", () => ({
49
+ useI18n: () => ({
50
+ language: "zh",
51
+ setLanguage: vi.fn(),
52
+ toggleLanguage: vi.fn(),
53
+ t: (key: string) => key,
54
+ }),
55
+ }));
56
+
57
+ vi.mock("@/components/doc-browser", () => ({
58
+ useDocBrowser: () => ({
59
+ open: mocks.docOpen,
60
+ }),
61
+ }));
62
+
63
+ vi.mock("@/hooks/useConfirmDialog", () => ({
64
+ useConfirmDialog: () => ({
65
+ confirm: mocks.confirm,
66
+ ConfirmDialog: () => null,
67
+ }),
68
+ }));
69
+
70
+ vi.mock("@/hooks/useMcpMarketplace", () => ({
71
+ useMcpMarketplaceItems: () => mocks.itemsQuery,
72
+ useMcpMarketplaceInstalled: () => mocks.installedQuery,
73
+ useInstallMcpMarketplaceItem: () => mocks.installMutation,
74
+ useManageMcpMarketplaceItem: () => mocks.manageMutation,
75
+ }));
76
+
77
+ function createItemsQuery(
78
+ overrides: Partial<ItemsQueryState> = {},
79
+ ): ItemsQueryState {
80
+ return {
81
+ data: undefined,
82
+ isLoading: false,
83
+ isFetching: false,
84
+ isError: false,
85
+ error: null,
86
+ ...overrides,
87
+ };
88
+ }
89
+
90
+ function createInstalledQuery(
91
+ overrides: Partial<InstalledQueryState> = {},
92
+ ): InstalledQueryState {
93
+ return {
94
+ data: {
95
+ type: "mcp",
96
+ total: 0,
97
+ specs: [],
98
+ records: [],
99
+ },
100
+ isLoading: false,
101
+ isFetching: false,
102
+ isError: false,
103
+ error: null,
104
+ ...overrides,
105
+ };
106
+ }
107
+
108
+ describe("McpMarketplacePage", () => {
109
+ beforeEach(() => {
110
+ mocks.installMutation.mutateAsync.mockReset();
111
+ mocks.manageMutation.mutateAsync.mockReset();
112
+ mocks.doctorMutation.mutateAsync.mockReset();
113
+ mocks.confirm.mockReset();
114
+ mocks.docOpen.mockReset();
115
+ mocks.itemsQuery = createItemsQuery();
116
+ mocks.installedQuery = createInstalledQuery();
117
+ });
118
+
119
+ it("prefers localized summary copy for the active language", () => {
120
+ mocks.itemsQuery = createItemsQuery({
121
+ data: {
122
+ total: 1,
123
+ page: 1,
124
+ pageSize: 12,
125
+ totalPages: 1,
126
+ sort: "relevance",
127
+ items: [
128
+ {
129
+ id: "mcp-chrome-devtools",
130
+ slug: "chrome-devtools",
131
+ type: "mcp",
132
+ name: "Chrome DevTools MCP",
133
+ summary:
134
+ "Connect MCP clients to Chrome DevTools for browser inspection and automation.",
135
+ summaryI18n: {
136
+ en: "Connect MCP clients to Chrome DevTools for browser inspection and automation.",
137
+ zh: "把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。",
138
+ },
139
+ tags: ["mcp", "browser"],
140
+ author: "Chrome DevTools",
141
+ install: {
142
+ kind: "template",
143
+ spec: "chrome-devtools",
144
+ command:
145
+ "nextclaw mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest",
146
+ },
147
+ updatedAt: "2026-03-19T00:00:00.000Z",
148
+ },
149
+ ],
150
+ },
151
+ });
152
+
153
+ render(<McpMarketplacePage />);
154
+
155
+ expect(
156
+ screen.getByText(
157
+ "把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。",
158
+ ),
159
+ ).toBeTruthy();
160
+ });
161
+
162
+ it("hides install button when an installed record matches by spec without catalog slug", () => {
163
+ mocks.itemsQuery = createItemsQuery({
164
+ data: {
165
+ total: 1,
166
+ page: 1,
167
+ pageSize: 12,
168
+ totalPages: 1,
169
+ sort: "relevance",
170
+ items: [
171
+ {
172
+ id: "mcp-chrome-devtools",
173
+ slug: "chrome-devtools",
174
+ type: "mcp",
175
+ name: "Chrome DevTools MCP",
176
+ summary:
177
+ "Connect MCP clients to Chrome DevTools for browser inspection and automation.",
178
+ summaryI18n: {
179
+ en: "Connect MCP clients to Chrome DevTools for browser inspection and automation.",
180
+ zh: "把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。",
181
+ },
182
+ tags: ["mcp", "browser"],
183
+ author: "Chrome DevTools",
184
+ install: {
185
+ kind: "template",
186
+ spec: "chrome-devtools",
187
+ command:
188
+ "nextclaw mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest",
189
+ },
190
+ updatedAt: "2026-03-19T00:00:00.000Z",
191
+ },
192
+ ],
193
+ },
194
+ });
195
+ mocks.installedQuery = createInstalledQuery({
196
+ data: {
197
+ type: "mcp",
198
+ total: 1,
199
+ specs: ["chrome-devtools"],
200
+ records: [
201
+ {
202
+ type: "mcp",
203
+ id: "chrome-devtools",
204
+ spec: "chrome-devtools",
205
+ label: "Chrome DevTools MCP",
206
+ enabled: true,
207
+ runtimeStatus: "enabled",
208
+ transport: "stdio",
209
+ scope: {
210
+ allAgents: true,
211
+ agents: [],
212
+ },
213
+ },
214
+ ],
215
+ },
216
+ });
217
+
218
+ render(<McpMarketplacePage />);
219
+
220
+ expect(screen.queryByText("Install")).toBeNull();
221
+ expect(screen.getByText("Disable")).toBeTruthy();
222
+ });
223
+ });