@nextclaw/ui 0.12.24 → 0.12.25

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 (206) hide show
  1. package/CHANGELOG.md +68 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-kk7qvZ-v.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DN0tvISH.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +54 -23
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +14 -0
  84. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  86. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  87. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  88. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  89. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  90. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +0 -2
  91. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  92. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  93. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  94. package/src/features/chat/pages/ncp-chat-page.tsx +5 -4
  95. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  96. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  97. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  98. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  99. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  100. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  101. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  102. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  103. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  104. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  105. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  106. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  107. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  108. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  109. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  110. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  111. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  112. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  113. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  114. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  115. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  116. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  117. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  118. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  119. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  120. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  121. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  122. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  123. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  124. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  125. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  126. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  127. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  128. package/src/index.css +8 -0
  129. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  130. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  131. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  132. package/src/platforms/desktop/index.ts +6 -0
  133. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  134. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  135. package/src/shared/components/common/brand-header.tsx +36 -16
  136. package/src/shared/components/config/provider-form-support.ts +2 -22
  137. package/src/shared/components/cron-config.tsx +12 -58
  138. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  139. package/src/shared/components/ui/select.tsx +19 -7
  140. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  141. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  142. package/src/shared/lib/api/types.ts +12 -1
  143. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  144. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  145. package/src/shared/lib/cron/index.ts +1 -0
  146. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  147. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  148. package/src/shared/lib/i18n/index.ts +20 -59
  149. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  150. package/src/shared/lib/provider-models/index.test.ts +39 -0
  151. package/src/shared/lib/provider-models/index.ts +1 -3
  152. package/src/shared/lib/ui-document-title/index.ts +0 -1
  153. package/tsconfig.json +1 -0
  154. package/vite.config.ts +1 -1
  155. package/vitest.config.ts +1 -1
  156. package/dist/assets/api-D2xRKmZd.js +0 -15
  157. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  158. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  159. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  160. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  161. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  162. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  163. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  164. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  165. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  166. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  167. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  168. package/dist/assets/folder-CeJKPx5P.js +0 -1
  169. package/dist/assets/hash-BqxRTZW5.js +0 -1
  170. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  171. package/dist/assets/index-D8MKmXtO.css +0 -1
  172. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  173. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  174. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  175. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  176. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  177. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  178. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  180. package/dist/assets/message-square-z_osm9c0.js +0 -1
  181. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  182. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  183. package/dist/assets/plus-D8eKFY7h.js +0 -1
  184. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  185. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  186. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  187. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  188. package/dist/assets/remote-BOxo9iwd.js +0 -1
  189. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  190. package/dist/assets/search-config-J4Htco-P.js +0 -1
  191. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  192. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  193. package/dist/assets/settings-drbWqzA4.js +0 -1
  194. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  195. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  196. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  197. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  198. package/dist/assets/use-config-38Ur-89i.js +0 -1
  199. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  200. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  201. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  202. package/dist/assets/x-CM-XDMpk.js +0 -1
  203. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  204. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  205. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  206. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -63,6 +63,11 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
63
63
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
64
64
  useMarketplaceItems: () => mocks.itemsQuery,
65
65
  useMarketplaceInstalled: () => mocks.installedQuery,
66
+ useMarketplaceSkillScenes: () => ({
67
+ data: { scenes: [] },
68
+ isLoading: false,
69
+ }),
70
+ useMarketplaceSkillSceneCounts: () => new Map(),
66
71
  useInstallMarketplaceItem: () => ({
67
72
  mutateAsync: vi.fn(),
68
73
  isPending: false,
@@ -103,7 +108,7 @@ function createItemsQuery(items: MarketplaceItemSummary[]): ItemsQueryState {
103
108
  data: {
104
109
  total: items.length,
105
110
  page: 1,
106
- pageSize: 12,
111
+ pageSize: 20,
107
112
  totalPages: 1,
108
113
  sort: "relevance",
109
114
  items,
@@ -162,7 +167,9 @@ describe("MarketplacePage detail loading", () => {
162
167
  await user.click(screen.getByText("Web Search"));
163
168
 
164
169
  expect(mocks.docOpen).toHaveBeenCalledTimes(1);
165
- expect(String(mocks.docOpen.mock.calls[0]?.[0])).toContain("Loading");
170
+ const loadingDetailHtml = decodeURIComponent(String(mocks.docOpen.mock.calls[0]?.[0]));
171
+ expect(loadingDetailHtml).toContain('aria-busy="true"');
172
+ expect(loadingDetailHtml).not.toContain("Loading");
166
173
  expect(mocks.docOpen.mock.calls[0]?.[1]).toMatchObject({
167
174
  dedupeKey: "marketplace:skill:web-search",
168
175
  kind: "content",
@@ -70,7 +70,7 @@ export function MarketplaceListSkeleton({ count }: {
70
70
  {Array.from({ length: count }, (_, index) => (
71
71
  <article
72
72
  key={`marketplace-skeleton-${index}`}
73
- className="rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
73
+ className="h-full rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
74
74
  >
75
75
  <div className="flex items-start justify-between gap-3.5">
76
76
  <div className="flex min-w-0 flex-1 gap-3">
@@ -28,6 +28,7 @@ const mocks = vi.hoisted(() => ({
28
28
  navigate: vi.fn(),
29
29
  docOpen: vi.fn(),
30
30
  confirm: vi.fn(),
31
+ routeParams: {} as { type?: string; scene?: string },
31
32
  itemsQuery: null as unknown as ItemsQueryState,
32
33
  installedQuery: null as unknown as InstalledQueryState,
33
34
  installMutation: {
@@ -48,7 +49,7 @@ vi.mock("react-router-dom", async () => {
48
49
  return {
49
50
  ...(actual as object),
50
51
  useNavigate: () => mocks.navigate,
51
- useParams: () => ({}),
52
+ useParams: () => mocks.routeParams,
52
53
  };
53
54
  });
54
55
 
@@ -73,6 +74,23 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
73
74
 
74
75
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
75
76
  useMarketplaceItems: () => mocks.itemsQuery,
77
+ useMarketplaceSkillScenes: () => ({
78
+ data: {
79
+ scenes: [
80
+ {
81
+ scene: "development-debugging",
82
+ title: "Development",
83
+ description: "Review, debug, analyze, and verify delivery work.",
84
+ count: 2,
85
+ },
86
+ ],
87
+ },
88
+ isLoading: false,
89
+ isFetching: false,
90
+ isError: false,
91
+ error: null,
92
+ }),
93
+ useMarketplaceSkillSceneCounts: () => new Map([["development-debugging", 2]]),
76
94
  useMarketplaceInstalled: () => mocks.installedQuery,
77
95
  useInstallMarketplaceItem: () => mocks.installMutation,
78
96
  useManageMarketplaceItem: () => mocks.manageMutation,
@@ -169,6 +187,7 @@ describe("MarketplacePage", () => {
169
187
  mocks.navigate.mockReset();
170
188
  mocks.docOpen.mockReset();
171
189
  mocks.confirm.mockReset();
190
+ mocks.routeParams = {};
172
191
  mocks.installMutation.mutateAsync.mockReset();
173
192
  mocks.manageMutation.mutate.mockReset();
174
193
  mocks.manageMutation.mutateAsync.mockReset();
@@ -193,7 +212,7 @@ describe("MarketplacePage", () => {
193
212
  container.querySelectorAll(
194
213
  '[data-testid="marketplace-list-skeleton"] > article',
195
214
  ),
196
- ).toHaveLength(12);
215
+ ).toHaveLength(36);
197
216
  });
198
217
 
199
218
  it("keeps loaded cards visible during background refresh", () => {
@@ -201,7 +220,7 @@ describe("MarketplacePage", () => {
201
220
  data: {
202
221
  total: 1,
203
222
  page: 1,
204
- pageSize: 12,
223
+ pageSize: 20,
205
224
  totalPages: 1,
206
225
  sort: "relevance",
207
226
  items: [createMarketplaceItem()],
@@ -220,7 +239,7 @@ describe("MarketplacePage", () => {
220
239
  data: {
221
240
  total: 1,
222
241
  page: 1,
223
- pageSize: 12,
242
+ pageSize: 20,
224
243
  totalPages: 1,
225
244
  sort: "relevance",
226
245
  items: [
@@ -256,7 +275,7 @@ describe("MarketplacePage", () => {
256
275
  data: {
257
276
  total: 1,
258
277
  page: 1,
259
- pageSize: 12,
278
+ pageSize: 20,
260
279
  totalPages: 1,
261
280
  sort: "relevance",
262
281
  items: [createPluginMarketplaceItem()],
@@ -277,7 +296,7 @@ describe("MarketplacePage", () => {
277
296
  data: {
278
297
  total: 2,
279
298
  page: 1,
280
- pageSize: 12,
299
+ pageSize: 20,
281
300
  totalPages: 1,
282
301
  sort: "relevance",
283
302
  items: [
@@ -23,7 +23,6 @@ import {
23
23
  } from "@/features/marketplace/hooks/use-marketplace";
24
24
  import {
25
25
  FilterPanel,
26
- MarketplaceListSkeleton,
27
26
  MarketplaceInfiniteScrollStatus,
28
27
  } from "@/features/marketplace/components/marketplace-page-parts";
29
28
  import {
@@ -34,25 +33,29 @@ import {
34
33
  buildCatalogLookup,
35
34
  buildInstalledRecordLookup,
36
35
  findCatalogItemForRecord,
37
- findInstalledRecordForItem,
38
36
  matchInstalledSearch,
39
37
  type InstalledRenderEntry,
40
38
  } from "@/features/marketplace/components/marketplace-page-data";
41
39
  import { buildGenericDetailDataUrl } from "@/features/marketplace/components/marketplace-detail-doc";
42
- import {
43
- MarketplaceListCard,
44
- type InstallState,
45
- type ManageState,
40
+ import type {
41
+ InstallState,
42
+ ManageState,
46
43
  } from "@/features/marketplace/components/marketplace-list-card";
44
+ import { MarketplaceCatalogGrid } from "@/features/marketplace/components/marketplace-catalog-grid";
45
+ import {
46
+ MarketplaceCuratedSceneView,
47
+ MarketplaceCuratedShelves,
48
+ } from "@/features/marketplace/components/curated-shelves/marketplace-curated-shelves";
49
+ import { useMarketplaceCuratedSceneRoute } from "@/features/marketplace/hooks/use-marketplace-curated-scene-route";
47
50
  import { t } from "@/shared/lib/i18n";
48
- import { PageHeader, PageLayout } from "@/app/components/layout/page-layout";
51
+ import { PageLayout } from "@/app/components/layout/page-layout";
49
52
  import { useEffect, useMemo, useRef, useState } from "react";
50
53
  import { useNavigate, useParams } from "react-router-dom";
51
54
  import { useInfiniteScrollLoader } from "@/shared/hooks/use-infinite-scroll-loader";
52
55
  import { Tabs } from "@/shared/components/ui/tabs-custom";
53
56
 
54
- const PAGE_SIZE = 12;
55
- const SKELETON_CARD_COUNT = PAGE_SIZE;
57
+ const PAGE_SIZE = 20;
58
+ const SKELETON_CARD_COUNT = 36;
56
59
 
57
60
  type ScopeType = "all" | "installed";
58
61
 
@@ -61,10 +64,43 @@ type MarketplacePageProps = {
61
64
  forcedType?: MarketplaceRouteType;
62
65
  };
63
66
 
67
+ function getMarketplaceCopyKeys(typeFilter: MarketplaceItemType) {
68
+ if (typeFilter === "plugin") {
69
+ return {
70
+ pageTitle: "marketplacePluginsPageTitle",
71
+ pageDescription: "marketplacePluginsPageDescription",
72
+ tabMarketplace: "marketplaceTabMarketplacePlugins",
73
+ tabInstalled: "marketplaceTabInstalledPlugins",
74
+ searchPlaceholder: "marketplaceSearchPlaceholderPlugins",
75
+ sectionCatalog: "marketplaceSectionPlugins",
76
+ sectionInstalled: "marketplaceSectionInstalledPlugins",
77
+ errorLoadData: "marketplaceErrorLoadingPluginsData",
78
+ errorLoadInstalled: "marketplaceErrorLoadingInstalledPlugins",
79
+ emptyData: "marketplaceNoPlugins",
80
+ emptyInstalled: "marketplaceNoInstalledPlugins",
81
+ installedCountSuffix: "marketplaceInstalledPluginsCountSuffix",
82
+ };
83
+ }
84
+ return {
85
+ pageTitle: "marketplaceSkillsPageTitle",
86
+ pageDescription: "marketplaceSkillsPageDescription",
87
+ tabMarketplace: "marketplaceTabMarketplaceSkills",
88
+ tabInstalled: "marketplaceTabInstalledSkills",
89
+ searchPlaceholder: "marketplaceSearchPlaceholderSkills",
90
+ sectionCatalog: "marketplaceSectionSkills",
91
+ sectionInstalled: "marketplaceSectionInstalledSkills",
92
+ errorLoadData: "marketplaceErrorLoadingSkillsData",
93
+ errorLoadInstalled: "marketplaceErrorLoadingInstalledSkills",
94
+ emptyData: "marketplaceNoSkills",
95
+ emptyInstalled: "marketplaceNoInstalledSkills",
96
+ installedCountSuffix: "marketplaceInstalledSkillsCountSuffix",
97
+ };
98
+ }
99
+
64
100
  export function MarketplacePage(props: MarketplacePageProps = {}) {
65
101
  const { forcedType } = props;
66
102
  const navigate = useNavigate();
67
- const params = useParams<{ type?: string }>();
103
+ const params = useParams<{ type?: string; scene?: string }>();
68
104
  const { language } = useI18n();
69
105
  const docBrowser = useDocBrowser();
70
106
 
@@ -94,36 +130,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
94
130
  [language],
95
131
  );
96
132
 
97
- const isPluginModule = typeFilter === "plugin";
98
- const copyKeys = isPluginModule
99
- ? {
100
- pageTitle: "marketplacePluginsPageTitle",
101
- pageDescription: "marketplacePluginsPageDescription",
102
- tabMarketplace: "marketplaceTabMarketplacePlugins",
103
- tabInstalled: "marketplaceTabInstalledPlugins",
104
- searchPlaceholder: "marketplaceSearchPlaceholderPlugins",
105
- sectionCatalog: "marketplaceSectionPlugins",
106
- sectionInstalled: "marketplaceSectionInstalledPlugins",
107
- errorLoadData: "marketplaceErrorLoadingPluginsData",
108
- errorLoadInstalled: "marketplaceErrorLoadingInstalledPlugins",
109
- emptyData: "marketplaceNoPlugins",
110
- emptyInstalled: "marketplaceNoInstalledPlugins",
111
- installedCountSuffix: "marketplaceInstalledPluginsCountSuffix",
112
- }
113
- : {
114
- pageTitle: "marketplaceSkillsPageTitle",
115
- pageDescription: "marketplaceSkillsPageDescription",
116
- tabMarketplace: "marketplaceTabMarketplaceSkills",
117
- tabInstalled: "marketplaceTabInstalledSkills",
118
- searchPlaceholder: "marketplaceSearchPlaceholderSkills",
119
- sectionCatalog: "marketplaceSectionSkills",
120
- sectionInstalled: "marketplaceSectionInstalledSkills",
121
- errorLoadData: "marketplaceErrorLoadingSkillsData",
122
- errorLoadInstalled: "marketplaceErrorLoadingInstalledSkills",
123
- emptyData: "marketplaceNoSkills",
124
- emptyInstalled: "marketplaceNoInstalledSkills",
125
- installedCountSuffix: "marketplaceInstalledSkillsCountSuffix",
126
- };
133
+ const copyKeys = getMarketplaceCopyKeys(typeFilter);
127
134
 
128
135
  const [searchText, setSearchText] = useState("");
129
136
  const [query, setQuery] = useState("");
@@ -145,9 +152,11 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
145
152
  }, [searchText]);
146
153
 
147
154
  const installedQuery = useMarketplaceInstalled(typeFilter);
155
+ const sceneParam = typeFilter === "skill" ? params.scene?.trim() : undefined;
148
156
 
149
157
  const itemsQuery = useMarketplaceItems({
150
- q: query || undefined,
158
+ q: sceneParam ? undefined : query || undefined,
159
+ scene: sceneParam,
151
160
  type: typeFilter,
152
161
  sort,
153
162
  pageSize: PAGE_SIZE,
@@ -160,7 +169,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
160
169
  !itemsQuery.hasNextPage ||
161
170
  itemsQuery.isFetchingNextPage,
162
171
  onLoadMore: () => itemsQuery.fetchNextPage(),
163
- watchValue: `${typeFilter}:${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`,
172
+ watchValue: `${typeFilter}:${scope}:${query}:${sceneParam ?? ""}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`,
164
173
  });
165
174
 
166
175
  useEffect(() => {
@@ -168,7 +177,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
168
177
  if (container && typeof container.scrollTo === "function") {
169
178
  container.scrollTo({ top: 0 });
170
179
  }
171
- }, [infiniteScroll.containerRef, query, scope, sort, typeFilter]);
180
+ }, [infiniteScroll.containerRef, query, sceneParam, scope, sort, typeFilter]);
172
181
 
173
182
  const installMutation = useInstallMarketplaceItem();
174
183
  const manageMutation = useManageMarketplaceItem();
@@ -263,15 +272,18 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
263
272
  const manageState: ManageState = {
264
273
  actionsByTarget: managingTargets,
265
274
  };
266
-
267
- const scopeTabs = [
268
- { id: "all", label: t(copyKeys.tabMarketplace) },
269
- {
270
- id: "installed",
271
- label: t(copyKeys.tabInstalled),
272
- count: installedQuery.data?.total ?? 0,
273
- },
274
- ];
275
+ const curatedSceneRoute = useMarketplaceCuratedSceneRoute({
276
+ items: allItems,
277
+ installedRecordLookup,
278
+ scene: sceneParam,
279
+ forcedType,
280
+ typeFilter,
281
+ scope,
282
+ searchText,
283
+ query,
284
+ showListSkeleton,
285
+ hasCatalogError: itemsQuery.isError,
286
+ });
275
287
 
276
288
  const handleInstall = async (item: MarketplaceItemSummary) => {
277
289
  const installSpec = item.install.spec;
@@ -419,11 +431,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
419
431
  title,
420
432
  typeLabel: detailConfig.typeLabel,
421
433
  spec: item.install.spec,
422
- summary,
423
- metadataRaw: t("loading"),
424
- contentRaw: t("loading"),
425
- tags: item.tags,
426
- author: item.author,
434
+ loading: true,
427
435
  }),
428
436
  openOptions,
429
437
  );
@@ -468,36 +476,44 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
468
476
 
469
477
  return (
470
478
  <PageLayout className="flex h-full min-h-0 flex-col pb-0">
471
- <PageHeader
472
- title={t(copyKeys.pageTitle)}
473
- description={t(copyKeys.pageDescription)}
474
- />
475
-
476
- <Tabs
477
- tabs={scopeTabs}
478
- activeTab={scope}
479
- onChange={(value) => setScope(value as ScopeType)}
480
- className="mb-4"
481
- />
482
-
483
- <FilterPanel
484
- scope={scope}
485
- searchText={searchText}
486
- searchPlaceholder={t(copyKeys.searchPlaceholder)}
487
- sort={sort}
488
- onSearchTextChange={setSearchText}
489
- onSortChange={setSort}
490
- />
479
+ {!curatedSceneRoute.isSceneRoute && (
480
+ <>
481
+ <Tabs
482
+ tabs={[
483
+ { id: "all", label: t(copyKeys.tabMarketplace) },
484
+ {
485
+ id: "installed",
486
+ label: t(copyKeys.tabInstalled),
487
+ count: installedQuery.data?.total ?? 0,
488
+ },
489
+ ]}
490
+ activeTab={scope}
491
+ onChange={(value) => setScope(value as ScopeType)}
492
+ className="mb-3"
493
+ />
494
+
495
+ <FilterPanel
496
+ scope={scope}
497
+ searchText={searchText}
498
+ searchPlaceholder={t(copyKeys.searchPlaceholder)}
499
+ sort={sort}
500
+ onSearchTextChange={setSearchText}
501
+ onSortChange={setSort}
502
+ />
503
+ </>
504
+ )}
491
505
 
492
506
  <section className="flex min-h-0 flex-1 flex-col">
493
- <div className="mb-3 flex items-center justify-between">
494
- <h3 className="text-[14px] font-semibold text-gray-900">
495
- {scope === "installed"
496
- ? t(copyKeys.sectionInstalled)
497
- : t(copyKeys.sectionCatalog)}
498
- </h3>
499
- <span className="text-[12px] text-gray-500">{listSummary}</span>
500
- </div>
507
+ {!curatedSceneRoute.isSceneRoute && !curatedSceneRoute.showShelves && (
508
+ <div className="mb-3 flex items-center justify-between">
509
+ <h3 className="text-[14px] font-semibold text-gray-900">
510
+ {scope === "installed"
511
+ ? t(copyKeys.sectionInstalled)
512
+ : t(copyKeys.sectionCatalog)}
513
+ </h3>
514
+ <span className="text-[12px] text-gray-500">{listSummary}</span>
515
+ </div>
516
+ )}
501
517
 
502
518
  {scope === "all" && itemsQuery.isError && (
503
519
  <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
@@ -515,55 +531,60 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
515
531
  className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1"
516
532
  aria-busy={showListSkeleton || itemsQuery.isFetchingNextPage}
517
533
  >
518
- <div
519
- data-testid={
520
- showListSkeleton ? "marketplace-list-skeleton" : undefined
521
- }
522
- className="grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3"
523
- >
524
- {showListSkeleton && (
525
- <MarketplaceListSkeleton count={SKELETON_CARD_COUNT} />
526
- )}
527
-
528
- {!showListSkeleton &&
529
- scope === "all" &&
530
- allItems.map((item) => (
531
- <MarketplaceListCard
532
- key={item.id}
533
- item={item}
534
- record={findInstalledRecordForItem(item, installedRecordLookup)}
535
- language={language}
536
- installState={installState}
537
- manageState={manageState}
538
- onOpen={() =>
539
- void openItemDetail(
540
- item,
541
- findInstalledRecordForItem(item, installedRecordLookup),
542
- )
543
- }
544
- onInstall={handleInstall}
545
- onManage={handleManage}
546
- />
547
- ))}
548
-
549
- {!showListSkeleton &&
550
- scope === "installed" &&
551
- installedEntries.map((entry) => (
552
- <MarketplaceListCard
553
- key={entry.key}
554
- item={entry.item}
555
- record={entry.record}
556
- language={language}
557
- installState={installState}
558
- manageState={manageState}
559
- onOpen={() => void openItemDetail(entry.item, entry.record)}
560
- onInstall={handleInstall}
561
- onManage={handleManage}
562
- />
563
- ))}
564
- </div>
534
+ {curatedSceneRoute.isSceneRoute && curatedSceneRoute.selectedScene && (
535
+ <MarketplaceCuratedSceneView
536
+ scene={curatedSceneRoute.selectedScene}
537
+ entries={curatedSceneRoute.sceneEntries}
538
+ isLoading={showListSkeleton}
539
+ language={language}
540
+ localeFallbacks={localeFallbacks}
541
+ installState={installState}
542
+ onBack={() => navigate(curatedSceneRoute.backPath)}
543
+ onOpen={(entry) => void openItemDetail(entry.item, entry.record)}
544
+ onInstall={handleInstall}
545
+ />
546
+ )}
547
+
548
+ {curatedSceneRoute.showShelves && (
549
+ <MarketplaceCuratedShelves
550
+ entries={curatedSceneRoute.entries}
551
+ scenes={curatedSceneRoute.scenes}
552
+ language={language}
553
+ installState={installState}
554
+ onOpen={(entry) => void openItemDetail(entry.item, entry.record)}
555
+ onInstall={handleInstall}
556
+ onOpenScene={(scene) =>
557
+ navigate(`${curatedSceneRoute.pathPrefix}/${scene}`)
558
+ }
559
+ />
560
+ )}
561
+
562
+ {!curatedSceneRoute.isSceneRoute && (
563
+ <MarketplaceCatalogGrid
564
+ scope={scope}
565
+ title={
566
+ scope === "installed"
567
+ ? t(copyKeys.sectionInstalled)
568
+ : t(copyKeys.sectionCatalog)
569
+ }
570
+ summary={listSummary}
571
+ showTitle={curatedSceneRoute.showShelves}
572
+ showListSkeleton={showListSkeleton}
573
+ skeletonCardCount={SKELETON_CARD_COUNT}
574
+ allItems={allItems}
575
+ installedEntries={installedEntries}
576
+ installedRecordLookup={installedRecordLookup}
577
+ language={language}
578
+ installState={installState}
579
+ manageState={manageState}
580
+ onOpen={(item, record) => void openItemDetail(item, record)}
581
+ onInstall={handleInstall}
582
+ onManage={handleManage}
583
+ />
584
+ )}
565
585
 
566
586
  {scope === "all" &&
587
+ !curatedSceneRoute.isSceneRoute &&
567
588
  !showListSkeleton &&
568
589
  !itemsQuery.isError &&
569
590
  allItems.length === 0 && (
@@ -572,6 +593,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
572
593
  </div>
573
594
  )}
574
595
  {scope === "installed" &&
596
+ !curatedSceneRoute.isSceneRoute &&
575
597
  !showListSkeleton &&
576
598
  !installedQuery.isError &&
577
599
  installedEntries.length === 0 && (
@@ -0,0 +1,97 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ MarketplaceItemType,
5
+ } from "@/shared/lib/api";
6
+ import { findInstalledRecordForItem } from "@/features/marketplace/components/marketplace-page-data";
7
+ import {
8
+ useMarketplaceSkillSceneCounts,
9
+ useMarketplaceSkillScenes,
10
+ } from "@/features/marketplace/hooks/use-marketplace";
11
+ import { useMemo } from "react";
12
+
13
+ type MarketplaceCuratedSceneRouteParams = {
14
+ items: MarketplaceItemSummary[];
15
+ installedRecordLookup: Map<string, MarketplaceInstalledRecord>;
16
+ scene?: string;
17
+ forcedType?: "plugins" | "skills";
18
+ typeFilter: MarketplaceItemType;
19
+ scope: "all" | "installed";
20
+ searchText: string;
21
+ query: string;
22
+ showListSkeleton: boolean;
23
+ hasCatalogError: boolean;
24
+ };
25
+
26
+ export function useMarketplaceCuratedSceneRoute(
27
+ params: MarketplaceCuratedSceneRouteParams,
28
+ ) {
29
+ const {
30
+ items,
31
+ installedRecordLookup,
32
+ scene,
33
+ forcedType,
34
+ typeFilter,
35
+ scope,
36
+ searchText,
37
+ query,
38
+ showListSkeleton,
39
+ hasCatalogError,
40
+ } = params;
41
+ const scenesQuery = useMarketplaceSkillScenes(typeFilter === "skill");
42
+ const rawScenes = useMemo(
43
+ () => scenesQuery.data?.scenes ?? [],
44
+ [scenesQuery.data?.scenes],
45
+ );
46
+ const fallbackCounts = useMarketplaceSkillSceneCounts(rawScenes, typeFilter === "skill");
47
+ const scenes = useMemo(
48
+ () =>
49
+ rawScenes.map((entry) => ({
50
+ ...entry,
51
+ count: typeof entry.count === "number"
52
+ ? entry.count
53
+ : fallbackCounts.get(entry.scene),
54
+ })),
55
+ [fallbackCounts, rawScenes],
56
+ );
57
+ const entries = useMemo(
58
+ () =>
59
+ items.map((item) => ({
60
+ item,
61
+ record: findInstalledRecordForItem(item, installedRecordLookup),
62
+ })),
63
+ [items, installedRecordLookup],
64
+ );
65
+ const selectedScene = useMemo(() => {
66
+ const normalizedScene = scene?.trim();
67
+ if (!normalizedScene) {
68
+ return undefined;
69
+ }
70
+ return scenes.find((entry) => entry.scene === normalizedScene) ?? {
71
+ scene: normalizedScene,
72
+ title: normalizedScene,
73
+ };
74
+ }, [scene, scenes]);
75
+ const isSceneRoute = typeFilter === "skill" && Boolean(scene?.trim());
76
+ const showShelves =
77
+ typeFilter === "skill" &&
78
+ scope === "all" &&
79
+ !searchText.trim() &&
80
+ !query &&
81
+ !showListSkeleton &&
82
+ !hasCatalogError &&
83
+ scenes.length > 0 &&
84
+ items.length >= 4 &&
85
+ !isSceneRoute;
86
+
87
+ return {
88
+ entries,
89
+ scenes,
90
+ selectedScene,
91
+ sceneEntries: entries,
92
+ isSceneRoute,
93
+ showShelves,
94
+ backPath: forcedType ? "/skills" : "/marketplace/skills",
95
+ pathPrefix: forcedType ? "/skills/scenes" : "/marketplace/skills/scenes",
96
+ };
97
+ }