@nextclaw/ui 0.12.9 → 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 (178) hide show
  1. package/CHANGELOG.md +61 -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-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.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-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.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-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.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-vZnghcFy.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-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.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-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.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-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -1,827 +0,0 @@
1
- /* eslint-disable max-lines-per-function */
2
- import type {
3
- MarketplaceInstalledRecord,
4
- MarketplaceItemSummary,
5
- MarketplaceManageAction,
6
- MarketplacePluginContentView,
7
- MarketplaceSkillContentView,
8
- MarketplaceSort,
9
- MarketplaceItemType
10
- } from '@/api/types';
11
- import { fetchMarketplacePluginContent, fetchMarketplaceSkillContent } from '@/api/marketplace';
12
- import { Tabs } from '@/components/ui/tabs-custom';
13
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
14
- import { useDocBrowser } from '@/components/doc-browser';
15
- import { useI18n } from '@/components/providers/I18nProvider';
16
- import { useConfirmDialog } from '@/hooks/useConfirmDialog';
17
- import {
18
- useInstallMarketplaceItem,
19
- useManageMarketplaceItem,
20
- useMarketplaceInstalled,
21
- useMarketplaceItems
22
- } from '@/hooks/useMarketplace';
23
- import {
24
- FilterPanel,
25
- MarketplaceListSkeleton,
26
- MarketplaceInfiniteScrollStatus
27
- } from '@/components/marketplace/marketplace-page-parts';
28
- import { buildLocaleFallbacks, pickLocalizedText } from '@/components/marketplace/marketplace-localization';
29
- import { t } from '@/lib/i18n';
30
- import { PageLayout, PageHeader } from '@/components/layout/page-layout';
31
- import { cn } from '@/lib/utils';
32
- import { useEffect, useMemo, useState } from 'react';
33
- import { useNavigate, useParams } from 'react-router-dom';
34
- import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
35
-
36
- const PAGE_SIZE = 12;
37
- const SKELETON_CARD_COUNT = PAGE_SIZE;
38
-
39
- type ScopeType = 'all' | 'installed';
40
-
41
- type InstallState = {
42
- installingSpecs: ReadonlySet<string>;
43
- };
44
-
45
- type ManageState = {
46
- actionsByTarget: ReadonlyMap<string, MarketplaceManageAction>;
47
- };
48
-
49
- type InstalledRenderEntry = {
50
- key: string;
51
- record: MarketplaceInstalledRecord;
52
- item?: MarketplaceItemSummary;
53
- };
54
-
55
- type MarketplaceRouteType = 'plugins' | 'skills';
56
- type MarketplacePageProps = {
57
- forcedType?: MarketplaceRouteType;
58
- };
59
-
60
- function normalizeMarketplaceKey(value: string | undefined): string {
61
- return (value ?? '').trim().toLowerCase();
62
- }
63
-
64
- function toLookupKey(type: MarketplaceItemSummary['type'], value: string | undefined): string {
65
- const normalized = normalizeMarketplaceKey(value);
66
- return normalized.length > 0 ? `${type}:${normalized}` : '';
67
- }
68
-
69
- function buildCatalogLookup(items: MarketplaceItemSummary[]): Map<string, MarketplaceItemSummary> {
70
- const lookup = new Map<string, MarketplaceItemSummary>();
71
-
72
- for (const item of items) {
73
- const candidates = [item.install.spec, item.slug, item.id];
74
- for (const candidate of candidates) {
75
- const lookupKey = toLookupKey(item.type, candidate);
76
- if (!lookupKey || lookup.has(lookupKey)) {
77
- continue;
78
- }
79
- lookup.set(lookupKey, item);
80
- }
81
- }
82
-
83
- return lookup;
84
- }
85
-
86
- function buildInstalledRecordLookup(records: MarketplaceInstalledRecord[]): Map<string, MarketplaceInstalledRecord> {
87
- const lookup = new Map<string, MarketplaceInstalledRecord>();
88
-
89
- for (const record of records) {
90
- const candidates = [record.spec, record.id, record.label];
91
- for (const candidate of candidates) {
92
- const lookupKey = toLookupKey(record.type, candidate);
93
- if (!lookupKey || lookup.has(lookupKey)) {
94
- continue;
95
- }
96
- lookup.set(lookupKey, record);
97
- }
98
- }
99
-
100
- return lookup;
101
- }
102
-
103
- function findInstalledRecordForItem(
104
- item: MarketplaceItemSummary,
105
- installedRecordLookup: Map<string, MarketplaceInstalledRecord>
106
- ): MarketplaceInstalledRecord | undefined {
107
- const candidates = [item.install.spec, item.slug, item.id];
108
- for (const candidate of candidates) {
109
- const lookupKey = toLookupKey(item.type, candidate);
110
- if (!lookupKey) {
111
- continue;
112
- }
113
- const record = installedRecordLookup.get(lookupKey);
114
- if (record) {
115
- return record;
116
- }
117
- }
118
- return undefined;
119
- }
120
-
121
- function findCatalogItemForRecord(
122
- record: MarketplaceInstalledRecord,
123
- catalogLookup: Map<string, MarketplaceItemSummary>
124
- ): MarketplaceItemSummary | undefined {
125
- const bySpec = catalogLookup.get(toLookupKey(record.type, record.spec));
126
- if (bySpec) {
127
- return bySpec;
128
- }
129
-
130
- const byId = catalogLookup.get(toLookupKey(record.type, record.id));
131
- if (byId) {
132
- return byId;
133
- }
134
-
135
- return catalogLookup.get(toLookupKey(record.type, record.label));
136
- }
137
-
138
- function matchInstalledSearch(
139
- record: MarketplaceInstalledRecord,
140
- item: MarketplaceItemSummary | undefined,
141
- query: string,
142
- localeFallbacks: string[]
143
- ): boolean {
144
- const normalizedQuery = normalizeMarketplaceKey(query);
145
- if (!normalizedQuery) {
146
- return true;
147
- }
148
-
149
- const localizedSummary = pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks);
150
- const values = [
151
- record.id,
152
- record.spec,
153
- record.label,
154
- item?.name,
155
- item?.slug,
156
- item?.summary,
157
- localizedSummary,
158
- ...(item?.tags ?? [])
159
- ];
160
-
161
- return values
162
- .map((value) => normalizeMarketplaceKey(value))
163
- .filter(Boolean)
164
- .some((value) => value.includes(normalizedQuery));
165
- }
166
-
167
- function getAvatarColor(text: string) {
168
- const colors = [
169
- 'bg-amber-600', 'bg-orange-500', 'bg-yellow-600', 'bg-emerald-600',
170
- 'bg-teal-600', 'bg-cyan-600', 'bg-stone-600', 'bg-rose-500', 'bg-violet-500'
171
- ];
172
- let hash = 0;
173
- for (let i = 0; i < text.length; i++) {
174
- hash = text.charCodeAt(i) + ((hash << 5) - hash);
175
- }
176
- return colors[Math.abs(hash) % colors.length];
177
- }
178
-
179
- function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
180
- const displayName = name || fallback;
181
- const letters = displayName.substring(0, 2).toUpperCase();
182
- const colorClass = getAvatarColor(displayName);
183
- return (
184
- <div className={cn('flex items-center justify-center w-10 h-10 rounded-xl text-white font-semibold text-sm shrink-0', colorClass)}>
185
- {letters}
186
- </div>
187
- );
188
- }
189
-
190
- function escapeHtml(text: string): string {
191
- return text
192
- .replace(/&/g, '&amp;')
193
- .replace(/</g, '&lt;')
194
- .replace(/>/g, '&gt;')
195
- .replace(/"/g, '&quot;')
196
- .replace(/'/g, '&#39;');
197
- }
198
-
199
- function buildGenericDetailDataUrl(params: {
200
- title: string;
201
- typeLabel: string;
202
- spec: string;
203
- summary?: string;
204
- description?: string;
205
- metadataRaw?: string;
206
- contentRaw?: string;
207
- sourceUrl?: string;
208
- sourceLabel?: string;
209
- tags?: string[];
210
- author?: string;
211
- }): string {
212
- const metadata = params.metadataRaw?.trim() || '-';
213
- const content = params.contentRaw?.trim() || '-';
214
- const summary = params.summary?.trim();
215
- const description = params.description?.trim();
216
- const shouldShowDescription = Boolean(description) && description !== summary;
217
-
218
- const html = `<!doctype html>
219
- <html>
220
- <head>
221
- <meta charset="utf-8" />
222
- <meta name="viewport" content="width=device-width, initial-scale=1" />
223
- <title>${escapeHtml(params.title)}</title>
224
- <style>
225
- :root { color-scheme: light; }
226
- body { margin: 0; background: #f7f9fc; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
227
- .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 40px; }
228
- .hero { border: 1px solid #dbeafe; border-radius: 16px; background: linear-gradient(180deg, #eff6ff, #ffffff); padding: 20px; box-shadow: 0 6px 20px rgba(30, 64, 175, 0.08); }
229
- .hero h1 { margin: 0; font-size: 26px; }
230
- .meta { margin-top: 8px; color: #475569; font-size: 13px; overflow-wrap: anywhere; word-break: break-word; }
231
- .summary { margin-top: 14px; font-size: 14px; line-height: 1.7; color: #334155; }
232
- .grid { display: grid; grid-template-columns: 260px 1fr; gap: 14px; margin-top: 16px; }
233
- .card { border: 1px solid #e2e8f0; background: #fff; border-radius: 14px; overflow: hidden; }
234
- .card h2 { margin: 0; padding: 12px 14px; font-size: 13px; font-weight: 700; color: #1d4ed8; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
235
- .card .body { padding: 12px 14px; font-size: 13px; color: #334155; line-height: 1.7; }
236
- .code { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; margin: 0; }
237
- .tags { margin-top: 10px; }
238
- .tag { display: inline-block; margin: 0 6px 6px 0; padding: 4px 9px; border-radius: 999px; background: #e0e7ff; color: #3730a3; font-size: 11px; }
239
- .source { color: #2563eb; text-decoration: none; overflow-wrap: anywhere; word-break: break-all; }
240
- @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
241
- </style>
242
- </head>
243
- <body>
244
- <main class="wrap">
245
- <section class="hero">
246
- <h1>${escapeHtml(params.title)}</h1>
247
- <div class="meta">${escapeHtml(params.typeLabel)} · ${escapeHtml(params.spec)}${params.author ? ` · ${escapeHtml(params.author)}` : ''}</div>
248
- ${summary ? `<p class="summary">${escapeHtml(summary)}</p>` : ''}
249
- ${shouldShowDescription ? `<p class="summary">${escapeHtml(description as string)}</p>` : ''}
250
- ${params.tags && params.tags.length > 0 ? `<div class="tags">${params.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}</div>` : ''}
251
- ${params.sourceUrl ? `<p class="meta" style="margin-top:12px;">${escapeHtml(params.sourceLabel ?? 'Source')}: <a class="source" href="${escapeHtml(params.sourceUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(params.sourceUrl)}</a></p>` : ''}
252
- </section>
253
-
254
- <section class="grid">
255
- <article class="card">
256
- <h2>Metadata</h2>
257
- <div class="body"><pre class="code">${escapeHtml(metadata)}</pre></div>
258
- </article>
259
- <article class="card">
260
- <h2>Content</h2>
261
- <div class="body"><pre class="code">${escapeHtml(content)}</pre></div>
262
- </article>
263
- </section>
264
- </main>
265
- </body>
266
- </html>`;
267
-
268
- return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
269
- }
270
-
271
- function MarketplaceListCard(props: {
272
- item?: MarketplaceItemSummary;
273
- record?: MarketplaceInstalledRecord;
274
- localeFallbacks: string[];
275
- installState: InstallState;
276
- manageState: ManageState;
277
- onOpen: () => void;
278
- onInstall: (item: MarketplaceItemSummary) => void;
279
- onManage: (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => void;
280
- }) {
281
- const { item, record, localeFallbacks, installState, manageState, onOpen, onInstall, onManage } = props;
282
- const pluginRecord = record?.type === 'plugin' ? record : undefined;
283
- const title = item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
284
- const summary = pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks)
285
- || (record ? t('marketplaceInstalledLocalSummary') : '');
286
- const spec = item?.install.spec ?? record?.spec ?? '';
287
-
288
- const targetId = record?.id || record?.spec;
289
- const busyAction = targetId ? manageState.actionsByTarget.get(targetId) : undefined;
290
- const busyForRecord = Boolean(busyAction);
291
-
292
- const canToggle = Boolean(pluginRecord);
293
- const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
294
- const canUninstallSkill = record?.type === 'skill' && record.source === 'workspace';
295
- const canUninstall = Boolean(canUninstallPlugin || canUninstallSkill);
296
-
297
- const isDisabled = record ? (record.enabled === false || record.runtimeStatus === 'disabled') : false;
298
- const installSpec = item?.install.spec;
299
- const isInstalling = typeof installSpec === 'string' && installState.installingSpecs.has(installSpec);
300
-
301
- return (
302
- <article
303
- onClick={onOpen}
304
- className="group bg-white border border-gray-200/40 hover:border-blue-300/80 rounded-2xl px-5 py-4 hover:shadow-md shadow-sm transition-all flex items-start gap-3.5 justify-between cursor-pointer"
305
- >
306
- <div className="flex gap-3 min-w-0 flex-1 h-full items-start">
307
- <ItemIcon name={title} fallback={spec || t('marketplaceTypeExtension')} />
308
- <div className="min-w-0 flex-1 flex flex-col justify-center h-full">
309
- <TooltipProvider delayDuration={400}>
310
- <Tooltip>
311
- <TooltipTrigger asChild>
312
- <div className="text-[14px] font-semibold text-gray-900 truncate leading-tight">{title}</div>
313
- </TooltipTrigger>
314
- <TooltipContent className="max-w-[300px] text-xs">
315
- {title}
316
- </TooltipContent>
317
- </Tooltip>
318
-
319
- <div className="flex items-center gap-1.5 mt-0.5 mb-1.5">
320
- {spec && (
321
- <Tooltip>
322
- <TooltipTrigger asChild>
323
- <span className="text-[11px] text-gray-400 truncate max-w-full font-mono">{spec}</span>
324
- </TooltipTrigger>
325
- <TooltipContent className="max-w-[300px] text-xs font-mono break-all">
326
- {spec}
327
- </TooltipContent>
328
- </Tooltip>
329
- )}
330
- </div>
331
-
332
- <Tooltip>
333
- <TooltipTrigger asChild>
334
- <p className="text-[12px] text-gray-500/90 line-clamp-1 transition-colors leading-relaxed text-left">{summary}</p>
335
- </TooltipTrigger>
336
- {summary && (
337
- <TooltipContent className="max-w-[400px] text-xs leading-relaxed">
338
- {summary}
339
- </TooltipContent>
340
- )}
341
- </Tooltip>
342
- </TooltipProvider>
343
- </div>
344
- </div>
345
-
346
- <div className="shrink-0 flex items-center h-full">
347
- {item && !record && (
348
- <button
349
- onClick={(event) => {
350
- event.stopPropagation();
351
- onInstall(item);
352
- }}
353
- disabled={isInstalling}
354
- className="inline-flex items-center gap-1.5 h-8 px-4 rounded-xl text-xs font-medium bg-primary text-white hover:bg-primary-600 disabled:opacity-50 transition-colors"
355
- >
356
- {isInstalling ? t('marketplaceInstalling') : t('marketplaceInstall')}
357
- </button>
358
- )}
359
-
360
- {pluginRecord && canToggle && (
361
- <button
362
- disabled={busyForRecord}
363
- onClick={(event) => {
364
- event.stopPropagation();
365
- onManage(isDisabled ? 'enable' : 'disable', pluginRecord);
366
- }}
367
- className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-gray-200/80 text-gray-600 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
368
- >
369
- {busyAction && busyAction !== 'uninstall'
370
- ? (busyAction === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
371
- : (isDisabled ? t('marketplaceEnable') : t('marketplaceDisable'))}
372
- </button>
373
- )}
374
-
375
- {record && canUninstall && (
376
- <button
377
- disabled={busyForRecord}
378
- onClick={(event) => {
379
- event.stopPropagation();
380
- onManage('uninstall', record);
381
- }}
382
- className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-rose-100 text-rose-500 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
383
- >
384
- {busyAction === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
385
- </button>
386
- )}
387
- </div>
388
- </article>
389
- );
390
- }
391
-
392
- export function MarketplacePage(props: MarketplacePageProps = {}) {
393
- const { forcedType } = props;
394
- const navigate = useNavigate();
395
- const params = useParams<{ type?: string }>();
396
- const { language } = useI18n();
397
- const docBrowser = useDocBrowser();
398
-
399
- const routeType: MarketplaceRouteType | null = useMemo(() => {
400
- if (forcedType === 'plugins' || forcedType === 'skills') {
401
- return forcedType;
402
- }
403
- if (params.type === 'plugins' || params.type === 'skills') {
404
- return params.type;
405
- }
406
- return null;
407
- }, [forcedType, params.type]);
408
-
409
- useEffect(() => {
410
- if (forcedType) {
411
- return;
412
- }
413
- if (!routeType) {
414
- navigate('/marketplace/plugins', { replace: true });
415
- }
416
- }, [forcedType, routeType, navigate]);
417
-
418
- const typeFilter: MarketplaceItemType = routeType === 'skills' ? 'skill' : 'plugin';
419
- const localeFallbacks = useMemo(() => buildLocaleFallbacks(language), [language]);
420
-
421
- const isPluginModule = typeFilter === 'plugin';
422
- const copyKeys = isPluginModule
423
- ? {
424
- pageTitle: 'marketplacePluginsPageTitle',
425
- pageDescription: 'marketplacePluginsPageDescription',
426
- tabMarketplace: 'marketplaceTabMarketplacePlugins',
427
- tabInstalled: 'marketplaceTabInstalledPlugins',
428
- searchPlaceholder: 'marketplaceSearchPlaceholderPlugins',
429
- sectionCatalog: 'marketplaceSectionPlugins',
430
- sectionInstalled: 'marketplaceSectionInstalledPlugins',
431
- errorLoadData: 'marketplaceErrorLoadingPluginsData',
432
- errorLoadInstalled: 'marketplaceErrorLoadingInstalledPlugins',
433
- emptyData: 'marketplaceNoPlugins',
434
- emptyInstalled: 'marketplaceNoInstalledPlugins',
435
- installedCountSuffix: 'marketplaceInstalledPluginsCountSuffix'
436
- }
437
- : {
438
- pageTitle: 'marketplaceSkillsPageTitle',
439
- pageDescription: 'marketplaceSkillsPageDescription',
440
- tabMarketplace: 'marketplaceTabMarketplaceSkills',
441
- tabInstalled: 'marketplaceTabInstalledSkills',
442
- searchPlaceholder: 'marketplaceSearchPlaceholderSkills',
443
- sectionCatalog: 'marketplaceSectionSkills',
444
- sectionInstalled: 'marketplaceSectionInstalledSkills',
445
- errorLoadData: 'marketplaceErrorLoadingSkillsData',
446
- errorLoadInstalled: 'marketplaceErrorLoadingInstalledSkills',
447
- emptyData: 'marketplaceNoSkills',
448
- emptyInstalled: 'marketplaceNoInstalledSkills',
449
- installedCountSuffix: 'marketplaceInstalledSkillsCountSuffix'
450
- };
451
-
452
- const [searchText, setSearchText] = useState('');
453
- const [query, setQuery] = useState('');
454
- const [scope, setScope] = useState<ScopeType>('all');
455
- const [sort, setSort] = useState<MarketplaceSort>('relevance');
456
- const [installingSpecs, setInstallingSpecs] = useState<ReadonlySet<string>>(new Set());
457
- const [managingTargets, setManagingTargets] = useState<ReadonlyMap<string, MarketplaceManageAction>>(new Map());
458
-
459
- useEffect(() => {
460
- const timer = setTimeout(() => {
461
- setQuery(searchText.trim());
462
- }, 250);
463
- return () => clearTimeout(timer);
464
- }, [searchText]);
465
-
466
- const installedQuery = useMarketplaceInstalled(typeFilter);
467
-
468
- const itemsQuery = useMarketplaceItems({
469
- q: query || undefined,
470
- type: typeFilter,
471
- sort,
472
- pageSize: PAGE_SIZE
473
- });
474
-
475
- const infiniteScroll = useInfiniteScrollLoader({
476
- disabled: scope !== 'all' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
477
- onLoadMore: () => itemsQuery.fetchNextPage(),
478
- watchValue: `${typeFilter}:${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
479
- });
480
-
481
- useEffect(() => {
482
- const container = infiniteScroll.containerRef.current;
483
- if (container && typeof container.scrollTo === 'function') {
484
- container.scrollTo({ top: 0 });
485
- }
486
- }, [infiniteScroll.containerRef, query, scope, sort, typeFilter]);
487
-
488
- const installMutation = useInstallMarketplaceItem();
489
- const manageMutation = useManageMarketplaceItem();
490
- const { confirm, ConfirmDialog } = useConfirmDialog();
491
-
492
- const installedRecords = useMemo(
493
- () => installedQuery.data?.records ?? [],
494
- [installedQuery.data?.records]
495
- );
496
-
497
- const allItems = useMemo(
498
- () => itemsQuery.data?.items ?? [],
499
- [itemsQuery.data?.items]
500
- );
501
-
502
- const catalogLookup = useMemo(
503
- () => buildCatalogLookup(allItems),
504
- [allItems]
505
- );
506
-
507
- const installedRecordLookup = useMemo(
508
- () => buildInstalledRecordLookup(installedRecords),
509
- [installedRecords]
510
- );
511
-
512
- const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
513
- const entries = installedRecords
514
- .filter((record) => record.type === typeFilter)
515
- .map((record) => ({
516
- key: `${record.type}:${record.spec}:${record.id ?? ''}`,
517
- record,
518
- item: findCatalogItemForRecord(record, catalogLookup)
519
- }))
520
- .filter((entry) => matchInstalledSearch(entry.record, entry.item, query, localeFallbacks));
521
-
522
- entries.sort((left, right) => {
523
- const leftTs = left.record.installedAt ? Date.parse(left.record.installedAt) : Number.NaN;
524
- const rightTs = right.record.installedAt ? Date.parse(right.record.installedAt) : Number.NaN;
525
- const leftValid = !Number.isNaN(leftTs);
526
- const rightValid = !Number.isNaN(rightTs);
527
-
528
- if (leftValid && rightValid && leftTs !== rightTs) {
529
- return rightTs - leftTs;
530
- }
531
-
532
- return left.record.spec.localeCompare(right.record.spec);
533
- });
534
-
535
- return entries;
536
- }, [installedRecords, typeFilter, catalogLookup, query, localeFallbacks]);
537
-
538
- const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
539
- const showCatalogSkeleton = scope === 'all' && itemsQuery.isLoading && !itemsQuery.data;
540
- const showInstalledSkeleton = scope === 'installed' && installedQuery.isLoading && !installedQuery.data;
541
- const showListSkeleton = showCatalogSkeleton || showInstalledSkeleton;
542
-
543
- const listSummary = useMemo(() => {
544
- if (scope === 'installed') {
545
- if (installedQuery.isLoading && !installedQuery.data) {
546
- return t('loading');
547
- }
548
- return `${installedEntries.length} ${t(copyKeys.installedCountSuffix)}`;
549
- }
550
-
551
- if (!itemsQuery.data) {
552
- return t('loading');
553
- }
554
-
555
- return `${allItems.length} / ${total}`;
556
- }, [scope, installedQuery.data, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total, copyKeys.installedCountSuffix]);
557
-
558
- const installState: InstallState = { installingSpecs };
559
-
560
- const manageState: ManageState = {
561
- actionsByTarget: managingTargets
562
- };
563
-
564
- const scopeTabs = [
565
- { id: 'all', label: t(copyKeys.tabMarketplace) },
566
- { id: 'installed', label: t(copyKeys.tabInstalled), count: installedQuery.data?.total ?? 0 }
567
- ];
568
-
569
- const handleInstall = async (item: MarketplaceItemSummary) => {
570
- const installSpec = item.install.spec;
571
- if (installingSpecs.has(installSpec)) {
572
- return;
573
- }
574
-
575
- setInstallingSpecs((prev) => {
576
- const next = new Set(prev);
577
- next.add(installSpec);
578
- return next;
579
- });
580
-
581
- try {
582
- await installMutation.mutateAsync({
583
- type: item.type,
584
- spec: installSpec,
585
- kind: item.install.kind,
586
- ...(item.type === 'skill'
587
- ? {
588
- skill: item.slug,
589
- installPath: `skills/${item.slug}`
590
- }
591
- : {})
592
- });
593
- } catch {
594
- // handled in mutation onError
595
- } finally {
596
- setInstallingSpecs((prev) => {
597
- if (!prev.has(installSpec)) {
598
- return prev;
599
- }
600
- const next = new Set(prev);
601
- next.delete(installSpec);
602
- return next;
603
- });
604
- }
605
- };
606
-
607
- const handleManage = async (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => {
608
- const targetId = record.id || record.spec;
609
- if (!targetId) {
610
- return;
611
- }
612
- if (managingTargets.has(targetId)) {
613
- return;
614
- }
615
-
616
- if (action === 'uninstall') {
617
- const confirmed = await confirm({
618
- title: `${t('marketplaceUninstallTitle')} ${targetId}?`,
619
- description: t('marketplaceUninstallDescription'),
620
- confirmLabel: t('marketplaceUninstall'),
621
- variant: 'destructive'
622
- });
623
- if (!confirmed) {
624
- return;
625
- }
626
- }
627
-
628
- setManagingTargets((previous) => {
629
- const next = new Map(previous);
630
- next.set(targetId, action);
631
- return next;
632
- });
633
-
634
- try {
635
- await manageMutation.mutateAsync({
636
- type: record.type,
637
- action,
638
- id: targetId,
639
- spec: record.spec
640
- });
641
- } finally {
642
- setManagingTargets((previous) => {
643
- if (!previous.has(targetId)) {
644
- return previous;
645
- }
646
- const next = new Map(previous);
647
- next.delete(targetId);
648
- return next;
649
- });
650
- }
651
- };
652
-
653
- const openItemDetail = async (item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord) => {
654
- const title = item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
655
-
656
- if (!item) {
657
- const url = buildGenericDetailDataUrl({
658
- title,
659
- typeLabel: record?.type === 'plugin' ? t('marketplaceTypePlugin') : t('marketplaceTypeSkill'),
660
- spec: record?.spec ?? '-',
661
- summary: t('marketplaceInstalledLocalSummary'),
662
- metadataRaw: JSON.stringify(record ?? {}, null, 2),
663
- contentRaw: '-'
664
- });
665
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
666
- return;
667
- }
668
-
669
- const summary = pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks);
670
-
671
- if (item.type === 'skill') {
672
- try {
673
- const content: MarketplaceSkillContentView = await fetchMarketplaceSkillContent(item.slug);
674
- const url = buildGenericDetailDataUrl({
675
- title,
676
- typeLabel: t('marketplaceTypeSkill'),
677
- spec: item.install.spec,
678
- summary,
679
- metadataRaw: content.metadataRaw,
680
- contentRaw: content.bodyRaw || content.raw,
681
- sourceUrl: content.sourceUrl,
682
- sourceLabel: `Source (${content.source})`,
683
- tags: item.tags,
684
- author: item.author
685
- });
686
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
687
- } catch (error) {
688
- const url = buildGenericDetailDataUrl({
689
- title,
690
- typeLabel: t('marketplaceTypeSkill'),
691
- spec: item.install.spec,
692
- summary,
693
- metadataRaw: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
694
- contentRaw: t('marketplaceOperationFailed')
695
- });
696
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
697
- }
698
- return;
699
- }
700
-
701
- try {
702
- const content: MarketplacePluginContentView = await fetchMarketplacePluginContent(item.slug);
703
- const url = buildGenericDetailDataUrl({
704
- title,
705
- typeLabel: t('marketplaceTypePlugin'),
706
- spec: item.install.spec,
707
- summary,
708
- metadataRaw: content.metadataRaw,
709
- contentRaw: content.bodyRaw || content.raw || item.summary,
710
- sourceUrl: content.sourceUrl,
711
- sourceLabel: `Source (${content.source})`,
712
- tags: item.tags,
713
- author: item.author
714
- });
715
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
716
- } catch (error) {
717
- const url = buildGenericDetailDataUrl({
718
- title,
719
- typeLabel: t('marketplaceTypePlugin'),
720
- spec: item.install.spec,
721
- summary,
722
- metadataRaw: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
723
- contentRaw: '-'
724
- });
725
- docBrowser.open(url, { newTab: true, title, kind: 'content' });
726
- }
727
- };
728
-
729
- return (
730
- <PageLayout className="flex h-full min-h-0 flex-col pb-0">
731
- <PageHeader title={t(copyKeys.pageTitle)} description={t(copyKeys.pageDescription)} />
732
-
733
- <Tabs
734
- tabs={scopeTabs}
735
- activeTab={scope}
736
- onChange={(value) => setScope(value as ScopeType)}
737
- className="mb-4"
738
- />
739
-
740
- <FilterPanel
741
- scope={scope}
742
- searchText={searchText}
743
- searchPlaceholder={t(copyKeys.searchPlaceholder)}
744
- sort={sort}
745
- onSearchTextChange={setSearchText}
746
- onSortChange={setSort}
747
- />
748
-
749
- <section className="flex min-h-0 flex-1 flex-col">
750
- <div className="flex items-center justify-between mb-3">
751
- <h3 className="text-[14px] font-semibold text-gray-900">
752
- {scope === 'installed' ? t(copyKeys.sectionInstalled) : t(copyKeys.sectionCatalog)}
753
- </h3>
754
- <span className="text-[12px] text-gray-500">{listSummary}</span>
755
- </div>
756
-
757
- {scope === 'all' && itemsQuery.isError && (
758
- <div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
759
- {t(copyKeys.errorLoadData)}: {itemsQuery.error.message}
760
- </div>
761
- )}
762
- {scope === 'installed' && installedQuery.isError && (
763
- <div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
764
- {t(copyKeys.errorLoadInstalled)}: {installedQuery.error.message}
765
- </div>
766
- )}
767
-
768
- <div
769
- ref={infiniteScroll.containerRef}
770
- className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1"
771
- aria-busy={showListSkeleton || itemsQuery.isFetchingNextPage}
772
- >
773
- <div
774
- data-testid={showListSkeleton ? 'marketplace-list-skeleton' : undefined}
775
- className="grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3"
776
- >
777
- {showListSkeleton && <MarketplaceListSkeleton count={SKELETON_CARD_COUNT} />}
778
-
779
- {!showListSkeleton && scope === 'all' && allItems.map((item) => (
780
- <MarketplaceListCard
781
- key={item.id}
782
- item={item}
783
- record={findInstalledRecordForItem(item, installedRecordLookup)}
784
- localeFallbacks={localeFallbacks}
785
- installState={installState}
786
- manageState={manageState}
787
- onOpen={() => void openItemDetail(item, findInstalledRecordForItem(item, installedRecordLookup))}
788
- onInstall={handleInstall}
789
- onManage={handleManage}
790
- />
791
- ))}
792
-
793
- {!showListSkeleton && scope === 'installed' && installedEntries.map((entry) => (
794
- <MarketplaceListCard
795
- key={entry.key}
796
- item={entry.item}
797
- record={entry.record}
798
- localeFallbacks={localeFallbacks}
799
- installState={installState}
800
- manageState={manageState}
801
- onOpen={() => void openItemDetail(entry.item, entry.record)}
802
- onInstall={handleInstall}
803
- onManage={handleManage}
804
- />
805
- ))}
806
- </div>
807
-
808
- {scope === 'all' && !showListSkeleton && !itemsQuery.isError && allItems.length === 0 && (
809
- <div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyData)}</div>
810
- )}
811
- {scope === 'installed' && !showListSkeleton && !installedQuery.isError && installedEntries.length === 0 && (
812
- <div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
813
- )}
814
-
815
- {scope === 'all' && !showCatalogSkeleton && !itemsQuery.isError && (
816
- <MarketplaceInfiniteScrollStatus
817
- hasMore={Boolean(itemsQuery.hasNextPage)}
818
- loading={itemsQuery.isFetchingNextPage}
819
- sentinelRef={infiniteScroll.sentinelRef}
820
- />
821
- )}
822
- </div>
823
- </section>
824
- <ConfirmDialog />
825
- </PageLayout>
826
- );
827
- }