@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
@@ -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
+ });