@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
@@ -1,580 +0,0 @@
1
- /* eslint-disable max-lines-per-function */
2
- import { useEffect, useMemo, useState } from 'react';
3
- import { useMutation } from '@tanstack/react-query';
4
- import { PageHeader, PageLayout } from '@/components/layout/page-layout';
5
- import { Tabs } from '@/components/ui/tabs-custom';
6
- import { Input } from '@/components/ui/input';
7
- import { Switch } from '@/components/ui/switch';
8
- import { Button } from '@/components/ui/button';
9
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
10
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
11
- import { Skeleton } from '@/components/ui/skeleton';
12
- import { useConfirmDialog } from '@/hooks/useConfirmDialog';
13
- import {
14
- fetchMcpMarketplaceContent,
15
- doctorMcpMarketplaceItem
16
- } from '@/api/mcp-marketplace';
17
- import {
18
- useInstallMcpMarketplaceItem,
19
- useManageMcpMarketplaceItem,
20
- useMcpMarketplaceInstalled,
21
- useMcpMarketplaceItems
22
- } from '@/hooks/useMcpMarketplace';
23
- import type {
24
- MarketplaceInstalledRecord,
25
- MarketplaceItemSummary,
26
- MarketplaceMcpDoctorResult,
27
- MarketplaceMcpInstallSpec,
28
- MarketplaceSort
29
- } from '@/api/types';
30
- import { useDocBrowser } from '@/components/doc-browser';
31
- import { useI18n } from '@/components/providers/I18nProvider';
32
- import {
33
- buildLocaleFallbacks,
34
- pickInstalledRecordDescription,
35
- pickLocalizedText
36
- } from '@/components/marketplace/marketplace-localization';
37
- import { t } from '@/lib/i18n';
38
- import { cn } from '@/lib/utils';
39
- import { MarketplaceInfiniteScrollStatus } from '@/components/marketplace/marketplace-page-parts';
40
- import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
41
-
42
- type ScopeType = 'catalog' | 'installed';
43
-
44
- const PAGE_SIZE = 12;
45
-
46
- function normalizeMarketplaceKey(value: string | undefined): string {
47
- return (value ?? '').trim().toLowerCase();
48
- }
49
-
50
- function buildInstalledRecordLookup(records: MarketplaceInstalledRecord[]): Map<string, MarketplaceInstalledRecord> {
51
- const lookup = new Map<string, MarketplaceInstalledRecord>();
52
-
53
- for (const record of records) {
54
- const candidates = [record.catalogSlug, record.spec, record.id, record.label];
55
- for (const candidate of candidates) {
56
- const normalized = normalizeMarketplaceKey(candidate);
57
- if (!normalized || lookup.has(normalized)) {
58
- continue;
59
- }
60
- lookup.set(normalized, record);
61
- }
62
- }
63
-
64
- return lookup;
65
- }
66
-
67
- function findInstalledRecordForItem(
68
- item: MarketplaceItemSummary,
69
- installedRecordLookup: Map<string, MarketplaceInstalledRecord>
70
- ): MarketplaceInstalledRecord | undefined {
71
- const candidates = [item.slug, item.install.spec, item.id, item.name];
72
- for (const candidate of candidates) {
73
- const normalized = normalizeMarketplaceKey(candidate);
74
- if (!normalized) {
75
- continue;
76
- }
77
- const record = installedRecordLookup.get(normalized);
78
- if (record) {
79
- return record;
80
- }
81
- }
82
- return undefined;
83
- }
84
-
85
- function buildDocDataUrl(title: string, metadata: string, content: string, sourceUrl?: string, summary?: string): string {
86
- const escape = (value: string) =>
87
- value
88
- .replace(/&/g, '&amp;')
89
- .replace(/</g, '&lt;')
90
- .replace(/>/g, '&gt;')
91
- .replace(/"/g, '&quot;')
92
- .replace(/'/g, '&#39;');
93
-
94
- const html = `<!doctype html>
95
- <html>
96
- <head>
97
- <meta charset="utf-8" />
98
- <meta name="viewport" content="width=device-width, initial-scale=1" />
99
- <title>${escape(title)}</title>
100
- <style>
101
- body { margin: 0; background: #f8fafc; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
102
- .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 40px; }
103
- .hero { border: 1px solid #dbeafe; border-radius: 16px; background: linear-gradient(180deg, #eff6ff, #ffffff); padding: 20px; }
104
- .hero h1 { margin: 0; font-size: 26px; }
105
- .grid { display: grid; grid-template-columns: 280px 1fr; gap: 14px; margin-top: 16px; }
106
- .card { border: 1px solid #e2e8f0; background: #fff; border-radius: 14px; overflow: hidden; }
107
- .card h2 { margin: 0; padding: 12px 14px; font-size: 13px; font-weight: 700; color: #1d4ed8; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
108
- .body { padding: 12px 14px; }
109
- pre { margin: 0; white-space: pre-wrap; line-height: 1.7; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
110
- a { color: #2563eb; text-decoration: none; }
111
- @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
112
- </style>
113
- </head>
114
- <body>
115
- <main class="wrap">
116
- <section class="hero">
117
- <h1>${escape(title)}</h1>
118
- ${summary ? `<p>${escape(summary)}</p>` : ''}
119
- ${sourceUrl ? `<p><a href="${escape(sourceUrl)}" target="_blank" rel="noopener noreferrer">${escape(sourceUrl)}</a></p>` : ''}
120
- </section>
121
- <section class="grid">
122
- <article class="card">
123
- <h2>Metadata</h2>
124
- <div class="body"><pre>${escape(metadata)}</pre></div>
125
- </article>
126
- <article class="card">
127
- <h2>Content</h2>
128
- <div class="body"><pre>${escape(content)}</pre></div>
129
- </article>
130
- </section>
131
- </main>
132
- </body>
133
- </html>`;
134
-
135
- return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
136
- }
137
-
138
- function readSummary(localeFallbacks: string[], item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord): string {
139
- const localizedSummary = pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks);
140
- if (localizedSummary) {
141
- return localizedSummary;
142
- }
143
-
144
- const localizedRecordDescription = pickInstalledRecordDescription(record, localeFallbacks);
145
- return localizedRecordDescription || t('marketplaceInstalledLocalSummary');
146
- }
147
-
148
- function readTransportLabel(item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord): string {
149
- if (record?.transport) {
150
- return record.transport.toUpperCase();
151
- }
152
- const install = item?.install as MarketplaceMcpInstallSpec | undefined;
153
- return (install?.transportTypes ?? []).map((entry) => entry.toUpperCase()).join(' / ') || 'MCP';
154
- }
155
-
156
- function InstallDialog(props: {
157
- item: MarketplaceItemSummary | null;
158
- open: boolean;
159
- pending: boolean;
160
- onOpenChange: (open: boolean) => void;
161
- onSubmit: (payload: { name: string; allAgents: boolean; inputs: Record<string, string> }) => Promise<void>;
162
- }) {
163
- const template = props.item?.install as MarketplaceMcpInstallSpec | undefined;
164
- const [name, setName] = useState('');
165
- const [allAgents, setAllAgents] = useState(true);
166
- const [inputs, setInputs] = useState<Record<string, string>>({});
167
-
168
- useEffect(() => {
169
- setName(template?.defaultName ?? '');
170
- setAllAgents(true);
171
- setInputs(
172
- Object.fromEntries((template?.inputs ?? []).map((field) => [field.id, field.defaultValue ?? '']))
173
- );
174
- }, [template, props.open]);
175
-
176
- return (
177
- <Dialog open={props.open} onOpenChange={props.onOpenChange}>
178
- <DialogContent>
179
- <DialogHeader>
180
- <DialogTitle>{t('marketplaceMcpInstallDialogTitle')}</DialogTitle>
181
- <DialogDescription>{props.item?.name ?? '-'}</DialogDescription>
182
- </DialogHeader>
183
-
184
- <div className="space-y-4">
185
- <div className="space-y-2">
186
- <div className="text-sm font-medium text-gray-800">{t('marketplaceMcpServerName')}</div>
187
- <Input value={name} onChange={(event) => setName(event.target.value)} placeholder={template?.defaultName ?? 'mcp-server'} />
188
- </div>
189
-
190
- <div className="flex items-center justify-between rounded-xl border border-gray-200 px-3 py-3">
191
- <div>
192
- <div className="text-sm font-medium text-gray-900">{t('marketplaceMcpAllAgents')}</div>
193
- <div className="text-xs text-gray-500">{t('marketplaceMcpAllAgentsDescription')}</div>
194
- </div>
195
- <Switch checked={allAgents} onCheckedChange={setAllAgents} />
196
- </div>
197
-
198
- {(template?.inputs ?? []).map((field) => (
199
- <div key={field.id} className="space-y-2">
200
- <div className="text-sm font-medium text-gray-800">{field.label}</div>
201
- {field.description && <div className="text-xs text-gray-500">{field.description}</div>}
202
- <Input
203
- type={field.secret ? 'password' : 'text'}
204
- value={inputs[field.id] ?? ''}
205
- onChange={(event) => setInputs((current) => ({ ...current, [field.id]: event.target.value }))}
206
- placeholder={field.defaultValue ?? ''}
207
- />
208
- </div>
209
- ))}
210
- </div>
211
-
212
- <DialogFooter>
213
- <Button variant="outline" onClick={() => props.onOpenChange(false)} disabled={props.pending}>
214
- {t('cancel')}
215
- </Button>
216
- <Button
217
- onClick={() => void props.onSubmit({ name, allAgents, inputs })}
218
- disabled={props.pending || !name.trim()}
219
- >
220
- {props.pending ? t('marketplaceInstalling') : t('marketplaceInstall')}
221
- </Button>
222
- </DialogFooter>
223
- </DialogContent>
224
- </Dialog>
225
- );
226
- }
227
-
228
- function DoctorDialog(props: {
229
- result: MarketplaceMcpDoctorResult | null;
230
- targetName: string | null;
231
- open: boolean;
232
- pending: boolean;
233
- onOpenChange: (open: boolean) => void;
234
- }) {
235
- return (
236
- <Dialog open={props.open} onOpenChange={props.onOpenChange}>
237
- <DialogContent>
238
- <DialogHeader>
239
- <DialogTitle>{t('marketplaceMcpDoctorTitle')}</DialogTitle>
240
- <DialogDescription>{props.targetName ?? '-'}</DialogDescription>
241
- </DialogHeader>
242
- {props.pending && <div className="text-sm text-gray-500">{t('loading')}</div>}
243
- {!props.pending && props.result && (
244
- <div className="space-y-3 text-sm text-gray-700">
245
- <div>{t('marketplaceMcpDoctorAccessible')}: {props.result.accessible ? t('statusReady') : t('marketplaceOperationFailed')}</div>
246
- <div>{t('marketplaceMcpDoctorTransport')}: {props.result.transport.toUpperCase()}</div>
247
- <div>{t('marketplaceMcpDoctorTools')}: {props.result.toolCount}</div>
248
- {props.result.error && <div className="rounded-lg bg-rose-50 px-3 py-2 text-rose-600">{props.result.error}</div>}
249
- </div>
250
- )}
251
- </DialogContent>
252
- </Dialog>
253
- );
254
- }
255
-
256
- export function McpMarketplacePage() {
257
- const [scope, setScope] = useState<ScopeType>('catalog');
258
- const [searchText, setSearchText] = useState('');
259
- const [query, setQuery] = useState('');
260
- const [sort, setSort] = useState<MarketplaceSort>('relevance');
261
- const [installingItem, setInstallingItem] = useState<MarketplaceItemSummary | null>(null);
262
- const [doctorTarget, setDoctorTarget] = useState<string | null>(null);
263
- const [doctorResult, setDoctorResult] = useState<MarketplaceMcpDoctorResult | null>(null);
264
- const { language } = useI18n();
265
- const docBrowser = useDocBrowser();
266
- const { confirm, ConfirmDialog } = useConfirmDialog();
267
- const localeFallbacks = useMemo(() => buildLocaleFallbacks(language), [language]);
268
-
269
- useEffect(() => {
270
- const timer = window.setTimeout(() => {
271
- setQuery(searchText.trim());
272
- }, 250);
273
- return () => window.clearTimeout(timer);
274
- }, [searchText]);
275
-
276
- const itemsQuery = useMcpMarketplaceItems({
277
- q: query || undefined,
278
- sort,
279
- pageSize: PAGE_SIZE
280
- });
281
- const installedQuery = useMcpMarketplaceInstalled();
282
-
283
- const infiniteScroll = useInfiniteScrollLoader({
284
- disabled: scope !== 'catalog' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
285
- onLoadMore: () => itemsQuery.fetchNextPage(),
286
- watchValue: `${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
287
- });
288
-
289
- useEffect(() => {
290
- const container = infiniteScroll.containerRef.current;
291
- if (container && typeof container.scrollTo === 'function') {
292
- container.scrollTo({ top: 0 });
293
- }
294
- }, [infiniteScroll.containerRef, query, scope, sort]);
295
-
296
- const installMutation = useInstallMcpMarketplaceItem();
297
- const manageMutation = useManageMcpMarketplaceItem();
298
- const doctorMutation = useMutation({
299
- mutationFn: doctorMcpMarketplaceItem,
300
- onSuccess: (result, name) => {
301
- setDoctorTarget(name);
302
- setDoctorResult(result);
303
- }
304
- });
305
-
306
- const installedRecordLookup = useMemo(() => {
307
- return buildInstalledRecordLookup(installedQuery.data?.records ?? []);
308
- }, [installedQuery.data?.records]);
309
-
310
- const installedRecords = useMemo(() => {
311
- const entries = installedQuery.data?.records ?? [];
312
- return entries.filter((record) => {
313
- const text = [
314
- record.id ?? '',
315
- record.label ?? '',
316
- record.catalogSlug ?? '',
317
- record.description ?? '',
318
- record.descriptionZh ?? ''
319
- ].join(' ').toLowerCase();
320
- return query ? text.includes(query.toLowerCase()) : true;
321
- });
322
- }, [installedQuery.data?.records, query]);
323
-
324
- const openDoc = async (item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord) => {
325
- const title = item?.name ?? record?.label ?? record?.id ?? 'MCP';
326
- const summary = readSummary(localeFallbacks, item, record);
327
- if (!item) {
328
- const url = buildDocDataUrl(
329
- title,
330
- JSON.stringify(record ?? {}, null, 2),
331
- t('marketplaceInstalledLocalSummary'),
332
- record?.docsUrl,
333
- summary
334
- );
335
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
336
- return;
337
- }
338
- try {
339
- const content = await fetchMcpMarketplaceContent(item.slug);
340
- const url = buildDocDataUrl(
341
- title,
342
- content.metadataRaw || JSON.stringify(item, null, 2),
343
- content.bodyRaw || content.raw,
344
- content.sourceUrl,
345
- summary
346
- );
347
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
348
- } catch (error) {
349
- const url = buildDocDataUrl(
350
- title,
351
- JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
352
- summary,
353
- undefined,
354
- summary
355
- );
356
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
357
- }
358
- };
359
-
360
- const handleInstall = async (payload: { name: string; allAgents: boolean; inputs: Record<string, string> }) => {
361
- if (!installingItem) {
362
- return;
363
- }
364
- await installMutation.mutateAsync({
365
- spec: installingItem.slug,
366
- name: payload.name.trim(),
367
- allAgents: payload.allAgents,
368
- inputs: payload.inputs
369
- });
370
- setInstallingItem(null);
371
- };
372
-
373
- const handleManage = async (action: 'enable' | 'disable' | 'remove', record: MarketplaceInstalledRecord) => {
374
- const target = record.id || record.spec;
375
- if (!target) {
376
- return;
377
- }
378
- if (action === 'remove') {
379
- const confirmed = await confirm({
380
- title: `${t('marketplaceMcpRemoveTitle')} ${target}?`,
381
- description: t('marketplaceMcpRemoveDescription'),
382
- confirmLabel: t('marketplaceMcpRemove'),
383
- variant: 'destructive'
384
- });
385
- if (!confirmed) {
386
- return;
387
- }
388
- }
389
- await manageMutation.mutateAsync({
390
- action,
391
- id: target,
392
- spec: record.spec
393
- });
394
- };
395
-
396
- const renderCard = (item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord) => {
397
- const installed = record ?? (item ? findInstalledRecordForItem(item, installedRecordLookup) : undefined);
398
- const name = item?.name ?? record?.label ?? record?.id ?? 'MCP';
399
- const summary = readSummary(localeFallbacks, item, record);
400
- const transport = readTransportLabel(item, record);
401
- const status = installed ? (installed.enabled === false ? t('marketplaceDisable') : t('statusReady')) : null;
402
-
403
- return (
404
- <article
405
- key={`${item?.id ?? record?.id ?? record?.spec}`}
406
- onClick={() => void openDoc(item, record)}
407
- className="cursor-pointer rounded-2xl border border-gray-200/70 bg-white p-4 shadow-sm transition hover:border-blue-300 hover:shadow-md"
408
- >
409
- <div className="flex items-start justify-between gap-3">
410
- <div className="min-w-0">
411
- <div className="text-sm font-semibold text-gray-900">{name}</div>
412
- <div className="mt-1 text-xs text-gray-500">{transport}</div>
413
- <div className="mt-2 line-clamp-2 text-sm text-gray-600">{summary}</div>
414
- <div className="mt-3 flex flex-wrap gap-2">
415
- {(item?.tags ?? []).map((tag) => (
416
- <span key={tag} className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] text-slate-600">{tag}</span>
417
- ))}
418
- {status && <span className="rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] text-emerald-700">{status}</span>}
419
- </div>
420
- </div>
421
-
422
- <div className="flex shrink-0 flex-col gap-2">
423
- {!installed && item && (
424
- <button
425
- className="rounded-xl bg-primary px-3 py-1.5 text-xs font-medium text-white"
426
- onClick={(event) => {
427
- event.stopPropagation();
428
- setInstallingItem(item);
429
- }}
430
- >
431
- {t('marketplaceInstall')}
432
- </button>
433
- )}
434
-
435
- {installed && (
436
- <>
437
- <button
438
- className="rounded-xl border border-gray-200 px-3 py-1.5 text-xs font-medium text-gray-700"
439
- onClick={(event) => {
440
- event.stopPropagation();
441
- void handleManage(installed.enabled === false ? 'enable' : 'disable', installed);
442
- }}
443
- >
444
- {installed.enabled === false ? t('marketplaceEnable') : t('marketplaceDisable')}
445
- </button>
446
- <button
447
- className="rounded-xl border border-blue-200 px-3 py-1.5 text-xs font-medium text-blue-700"
448
- onClick={(event) => {
449
- event.stopPropagation();
450
- setDoctorTarget(installed.id ?? null);
451
- setDoctorResult(null);
452
- void doctorMutation.mutateAsync(installed.id ?? '');
453
- }}
454
- >
455
- {t('marketplaceMcpDoctor')}
456
- </button>
457
- <button
458
- className="rounded-xl border border-rose-200 px-3 py-1.5 text-xs font-medium text-rose-600"
459
- onClick={(event) => {
460
- event.stopPropagation();
461
- void handleManage('remove', installed);
462
- }}
463
- >
464
- {t('marketplaceMcpRemove')}
465
- </button>
466
- </>
467
- )}
468
- </div>
469
- </div>
470
- </article>
471
- );
472
- };
473
-
474
- return (
475
- <PageLayout className="flex h-full min-h-0 flex-col pb-0">
476
- <PageHeader title={t('marketplaceMcpPageTitle')} description={t('marketplaceMcpPageDescription')} />
477
-
478
- <Tabs
479
- tabs={[
480
- { id: 'catalog', label: t('marketplaceMcpTabCatalog') },
481
- { id: 'installed', label: t('marketplaceMcpTabInstalled'), count: installedQuery.data?.total ?? 0 }
482
- ]}
483
- activeTab={scope}
484
- onChange={(value) => setScope(value as ScopeType)}
485
- className="mb-4"
486
- />
487
-
488
- <div className="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
489
- <Input
490
- value={searchText}
491
- onChange={(event) => setSearchText(event.target.value)}
492
- placeholder={t('marketplaceMcpSearchPlaceholder')}
493
- className="md:max-w-sm"
494
- />
495
-
496
- <Select value={sort} onValueChange={(value) => setSort(value as MarketplaceSort)}>
497
- <SelectTrigger className="h-9 w-[180px] rounded-lg">
498
- <SelectValue />
499
- </SelectTrigger>
500
- <SelectContent>
501
- <SelectItem value="relevance">{t('marketplaceSortRelevance')}</SelectItem>
502
- <SelectItem value="updated">{t('marketplaceSortUpdated')}</SelectItem>
503
- </SelectContent>
504
- </Select>
505
- </div>
506
-
507
- <section className="flex min-h-0 flex-1 flex-col">
508
- <div className="mb-3 flex items-center justify-between">
509
- <h3 className="text-sm font-semibold text-gray-900">
510
- {scope === 'catalog' ? t('marketplaceMcpSectionCatalog') : t('marketplaceMcpSectionInstalled')}
511
- </h3>
512
- <span className="text-xs text-gray-500">
513
- {scope === 'catalog' ? (itemsQuery.data?.total ?? 0) : (installedQuery.data?.total ?? 0)}
514
- </span>
515
- </div>
516
-
517
- <div
518
- ref={infiniteScroll.containerRef}
519
- className="min-h-0 flex-1 overflow-y-auto pr-1"
520
- aria-busy={itemsQuery.isLoading || itemsQuery.isFetchingNextPage}
521
- >
522
- <div className={cn('grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3')}>
523
- {scope === 'catalog' && itemsQuery.isLoading && Array.from({ length: 6 }, (_, index) => (
524
- <div key={index} className="rounded-2xl border border-gray-200/70 bg-white p-4 shadow-sm">
525
- <Skeleton className="h-4 w-32" />
526
- <Skeleton className="mt-2 h-3 w-20" />
527
- <Skeleton className="mt-3 h-3 w-full" />
528
- <Skeleton className="mt-2 h-3 w-2/3" />
529
- </div>
530
- ))}
531
-
532
- {scope === 'catalog' && !itemsQuery.isLoading && (itemsQuery.data?.items ?? []).map((item) => renderCard(item))}
533
- {scope === 'installed' && installedRecords.map((record) => renderCard(undefined, record))}
534
- </div>
535
-
536
- {scope === 'catalog' && itemsQuery.isError && (
537
- <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
538
- {itemsQuery.error.message}
539
- </div>
540
- )}
541
- {scope === 'installed' && installedQuery.isError && (
542
- <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
543
- {installedQuery.error.message}
544
- </div>
545
- )}
546
- {scope === 'catalog' && !itemsQuery.isLoading && (itemsQuery.data?.items?.length ?? 0) === 0 && (
547
- <div className="py-8 text-center text-sm text-gray-500">{t('marketplaceNoMcp')}</div>
548
- )}
549
- {scope === 'installed' && installedRecords.length === 0 && (
550
- <div className="py-8 text-center text-sm text-gray-500">{t('marketplaceNoInstalledMcp')}</div>
551
- )}
552
-
553
- {scope === 'catalog' && !itemsQuery.isError && (
554
- <MarketplaceInfiniteScrollStatus
555
- hasMore={Boolean(itemsQuery.hasNextPage)}
556
- loading={itemsQuery.isFetchingNextPage}
557
- sentinelRef={infiniteScroll.sentinelRef}
558
- />
559
- )}
560
- </div>
561
- </section>
562
-
563
- <InstallDialog
564
- item={installingItem}
565
- open={Boolean(installingItem)}
566
- pending={installMutation.isPending}
567
- onOpenChange={(open) => !open && setInstallingItem(null)}
568
- onSubmit={handleInstall}
569
- />
570
- <DoctorDialog
571
- open={Boolean(doctorTarget)}
572
- targetName={doctorTarget}
573
- result={doctorResult}
574
- pending={doctorMutation.isPending}
575
- onOpenChange={(open) => !open && setDoctorTarget(null)}
576
- />
577
- <ConfirmDialog />
578
- </PageLayout>
579
- );
580
- }
@@ -1,103 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { RemoteAccessPage } from '@/components/remote/RemoteAccessPage';
4
- import { setLanguage } from '@/lib/i18n';
5
- import { useRemoteAccessStore } from '@/remote/stores/remote-access.store';
6
-
7
- const mocks = vi.hoisted(() => ({
8
- reauthorizeRemoteAccess: vi.fn(),
9
- repairRemoteAccess: vi.fn(),
10
- enableRemoteAccess: vi.fn(),
11
- disableRemoteAccess: vi.fn(),
12
- syncStatus: vi.fn(),
13
- openNextClawWeb: vi.fn(),
14
- statusQuery: {
15
- data: undefined as unknown,
16
- isLoading: false
17
- }
18
- }));
19
-
20
- vi.mock('@/hooks/useRemoteAccess', () => ({
21
- useRemoteStatus: () => mocks.statusQuery
22
- }));
23
-
24
- vi.mock('@/presenter/app-presenter-context', () => ({
25
- useAppPresenter: () => ({
26
- remoteAccessManager: {
27
- reauthorizeRemoteAccess: mocks.reauthorizeRemoteAccess,
28
- repairRemoteAccess: mocks.repairRemoteAccess,
29
- enableRemoteAccess: mocks.enableRemoteAccess,
30
- disableRemoteAccess: mocks.disableRemoteAccess,
31
- syncStatus: mocks.syncStatus
32
- },
33
- accountManager: {
34
- openNextClawWeb: mocks.openNextClawWeb
35
- }
36
- })
37
- }));
38
-
39
- describe('RemoteAccessPage', () => {
40
- beforeEach(() => {
41
- setLanguage('zh');
42
- mocks.reauthorizeRemoteAccess.mockReset();
43
- mocks.repairRemoteAccess.mockReset();
44
- mocks.enableRemoteAccess.mockReset();
45
- mocks.disableRemoteAccess.mockReset();
46
- mocks.syncStatus.mockReset();
47
- mocks.openNextClawWeb.mockReset();
48
- useRemoteAccessStore.setState({
49
- enabled: false,
50
- deviceName: '',
51
- platformApiBase: '',
52
- draftTouched: false,
53
- advancedOpen: false,
54
- actionLabel: null,
55
- doctor: null
56
- });
57
- mocks.statusQuery = {
58
- data: {
59
- account: {
60
- loggedIn: true,
61
- email: 'user@example.com',
62
- apiBase: 'https://ai-gateway-api.nextclaw.io/v1',
63
- platformBase: 'https://ai-gateway-api.nextclaw.io'
64
- },
65
- settings: {
66
- enabled: true,
67
- deviceName: 'MacBook Pro',
68
- platformApiBase: 'https://ai-gateway-api.nextclaw.io/v1'
69
- },
70
- service: {
71
- running: true,
72
- currentProcess: false
73
- },
74
- localOrigin: 'http://127.0.0.1:55667',
75
- configuredEnabled: true,
76
- platformBase: 'https://ai-gateway-api.nextclaw.io',
77
- runtime: {
78
- enabled: true,
79
- mode: 'service',
80
- state: 'error',
81
- lastError: 'Invalid or expired token.',
82
- updatedAt: '2026-03-23T00:00:00.000Z'
83
- }
84
- },
85
- isLoading: false
86
- };
87
- });
88
-
89
- it('shows a user-facing reauthorization flow instead of raw token errors', async () => {
90
- const user = userEvent.setup();
91
-
92
- render(<RemoteAccessPage />);
93
-
94
- expect(screen.getByText('登录已过期,请重新登录 NextClaw')).toBeTruthy();
95
- expect(screen.getByText('重新登录并恢复远程访问')).toBeTruthy();
96
- expect(screen.queryByText('Invalid or expired token.')).toBeNull();
97
-
98
- await user.click(screen.getByRole('button', { name: '重新登录并恢复远程访问' }));
99
-
100
- expect(mocks.reauthorizeRemoteAccess).toHaveBeenCalledTimes(1);
101
- expect(mocks.repairRemoteAccess).not.toHaveBeenCalled();
102
- });
103
- });