@nextclaw/ui 0.12.23 → 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 (215) hide show
  1. package/CHANGELOG.md +136 -0
  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-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-DsYTOyq7.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-V75WQJ2s.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-D1RNsTn_.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-BMyiifTA.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-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-CvKngoNI.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-BywQeHJj.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 +56 -25
  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 +119 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
  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-hydrated-ncp-agent.test.tsx +6 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
  85. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  87. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  88. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  89. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  90. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  91. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  92. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  93. package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
  94. package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
  95. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
  96. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  97. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  98. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  99. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  100. package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
  101. package/src/features/chat/stores/chat-session-list.store.ts +0 -3
  102. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  103. package/src/features/chat/types/chat-stream.types.ts +1 -1
  104. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  105. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  106. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  107. package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
  108. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  109. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  110. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  112. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  113. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  114. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  116. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  117. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  118. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  119. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  120. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  121. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  122. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  123. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  124. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  125. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  126. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  127. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  128. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  129. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  130. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  131. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  132. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  133. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  134. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  135. package/src/index.css +8 -0
  136. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  137. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  138. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  139. package/src/platforms/desktop/index.ts +6 -0
  140. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  141. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  142. package/src/shared/components/common/brand-header.tsx +36 -16
  143. package/src/shared/components/config/provider-form-support.ts +2 -22
  144. package/src/shared/components/cron-config.tsx +12 -58
  145. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  146. package/src/shared/components/ui/select.tsx +19 -7
  147. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  148. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  149. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  150. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  151. package/src/shared/lib/api/types.ts +12 -1
  152. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  153. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  154. package/src/shared/lib/cron/index.ts +1 -0
  155. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  156. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  157. package/src/shared/lib/i18n/index.ts +20 -59
  158. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  159. package/src/shared/lib/provider-models/index.test.ts +39 -0
  160. package/src/shared/lib/provider-models/index.ts +1 -3
  161. package/src/shared/lib/ui-document-title/index.ts +0 -1
  162. package/tsconfig.json +1 -0
  163. package/vite.config.ts +1 -1
  164. package/vitest.config.ts +1 -1
  165. package/dist/assets/api-BGd3rgv_.js +0 -15
  166. package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
  167. package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
  168. package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
  169. package/dist/assets/chat-DGM6K3Qs.js +0 -61
  170. package/dist/assets/chat-page-DpmXMWNS.js +0 -1
  171. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  172. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  173. package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
  174. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  175. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  176. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  177. package/dist/assets/folder-CeJKPx5P.js +0 -1
  178. package/dist/assets/hash-BqxRTZW5.js +0 -1
  179. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  180. package/dist/assets/index-BrEdR78s.js +0 -2
  181. package/dist/assets/index-D8MKmXtO.css +0 -1
  182. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  183. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  184. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  185. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  186. package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
  187. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
  189. package/dist/assets/message-square-z_osm9c0.js +0 -1
  190. package/dist/assets/model-config-Cmruiqdx.js +0 -1
  191. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  192. package/dist/assets/plus-D8eKFY7h.js +0 -1
  193. package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
  194. package/dist/assets/providers-list-gg7LrfuB.js +0 -1
  195. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  196. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  197. package/dist/assets/remote-Db2M39Cv.js +0 -1
  198. package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
  199. package/dist/assets/search-config-0VTPpz-w.js +0 -1
  200. package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
  201. package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
  202. package/dist/assets/settings-drbWqzA4.js +0 -1
  203. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  204. package/dist/assets/theme-provider-COAwWFv8.js +0 -2
  205. package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
  206. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  207. package/dist/assets/use-config-DTwhNDQE.js +0 -1
  208. package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
  209. package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
  210. package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
  211. package/dist/assets/x-CM-XDMpk.js +0 -1
  212. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  213. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  214. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  215. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -0,0 +1,355 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ MarketplaceSceneView,
5
+ } from "@/shared/lib/api";
6
+ import type { InstallState } from "@/features/marketplace/components/marketplace-list-card";
7
+ import {
8
+ buildLocaleFallbacks,
9
+ } from "@/features/marketplace/components/marketplace-localization";
10
+ import { MarketplaceListSkeleton } from "@/features/marketplace/components/marketplace-page-parts";
11
+ import { cn } from "@/shared/lib/utils";
12
+ import {
13
+ ArrowLeft,
14
+ Clock3,
15
+ Compass,
16
+ } from "lucide-react";
17
+ import { type ComponentType } from "react";
18
+ import { SkillShelfCard } from "@/features/marketplace/components/curated-shelves/marketplace-shelf-card";
19
+ import {
20
+ MARKETPLACE_SHELF_FALLBACK_VISUAL,
21
+ MARKETPLACE_SHELF_SCENE_VISUALS,
22
+ MARKETPLACE_SHELF_TONE_STYLES,
23
+ type MarketplaceShelfLocalizedText,
24
+ type MarketplaceShelfSceneVisual,
25
+ } from "@/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config";
26
+
27
+ const SCENE_CARD_GRID_CLASS =
28
+ "grid grid-cols-[repeat(auto-fill,minmax(240px,320px))] justify-start gap-3";
29
+ const SCENE_SKELETON_CARD_COUNT = 24;
30
+
31
+ export type MarketplaceShelfEntry = {
32
+ item: MarketplaceItemSummary;
33
+ record?: MarketplaceInstalledRecord;
34
+ };
35
+
36
+ export function MarketplaceCuratedShelves(props: {
37
+ entries: MarketplaceShelfEntry[];
38
+ scenes: MarketplaceSceneView[];
39
+ language: string;
40
+ installState: InstallState;
41
+ onOpen: (entry: MarketplaceShelfEntry) => void;
42
+ onInstall: (item: MarketplaceItemSummary) => void;
43
+ onOpenScene: (scene: string) => void;
44
+ }) {
45
+ const {
46
+ entries,
47
+ scenes,
48
+ language,
49
+ installState,
50
+ onOpen,
51
+ onInstall,
52
+ onOpenScene,
53
+ } = props;
54
+ const localeFallbacks = buildLocaleFallbacks(language);
55
+ const recentEntries = [...entries]
56
+ .sort((left, right) => compareUpdatedAt(left.item, right.item))
57
+ .slice(0, 6);
58
+
59
+ return (
60
+ <div className="mb-4 space-y-5">
61
+ <section className="space-y-2.5">
62
+ <ShelfHeader
63
+ icon={Compass}
64
+ title={readLocalized({ zh: "场景", en: "Scenes" }, language)}
65
+ description={readLocalized(
66
+ {
67
+ zh: "按使用场景浏览适合的技能组合。",
68
+ en: "Browse skills by how you plan to use them.",
69
+ },
70
+ language,
71
+ )}
72
+ />
73
+ <div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
74
+ {scenes.map((scene) => (
75
+ <GoalCard
76
+ key={scene.scene}
77
+ scene={scene}
78
+ language={language}
79
+ onSelect={onOpenScene}
80
+ />
81
+ ))}
82
+ </div>
83
+ </section>
84
+
85
+ {recentEntries.length > 0 && (
86
+ <ShelfItemRow
87
+ icon={Clock3}
88
+ title={readLocalized({ zh: "最近更新", en: "Recently updated" }, language)}
89
+ description={readLocalized(
90
+ {
91
+ zh: "看看生态最近在补齐哪些能力。",
92
+ en: "See what the ecosystem has been improving lately.",
93
+ },
94
+ language,
95
+ )}
96
+ entries={recentEntries}
97
+ language={language}
98
+ localeFallbacks={localeFallbacks}
99
+ installState={installState}
100
+ onOpen={onOpen}
101
+ onInstall={onInstall}
102
+ />
103
+ )}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function GoalCard(props: {
109
+ scene: MarketplaceSceneView;
110
+ language: string;
111
+ onSelect: (scene: string) => void;
112
+ }) {
113
+ const { scene, language, onSelect } = props;
114
+ const visual = resolveSceneVisual(scene.scene);
115
+ const Icon = visual.icon;
116
+ const tone = MARKETPLACE_SHELF_TONE_STYLES[visual.tone];
117
+
118
+ return (
119
+ <button
120
+ type="button"
121
+ onClick={() => onSelect(scene.scene)}
122
+ className={cn(
123
+ "group flex min-h-[74px] flex-col justify-center rounded-lg border px-3 py-2.5 text-left shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50/70",
124
+ tone.card,
125
+ )}
126
+ >
127
+ <div className="min-w-0">
128
+ <div className="flex min-w-0 items-center gap-2">
129
+ <span
130
+ className={cn(
131
+ "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border",
132
+ tone.icon,
133
+ )}
134
+ >
135
+ <Icon className="h-3.5 w-3.5" />
136
+ </span>
137
+ <div className="min-w-0 flex-1 truncate text-[13px] font-semibold text-gray-950">
138
+ {readSceneTitle(scene, visual, language)}
139
+ </div>
140
+ {typeof scene.count === "number" && (
141
+ <span className="shrink-0 text-[11px] font-medium text-gray-400">
142
+ {readSceneCount(scene.count, language)}
143
+ </span>
144
+ )}
145
+ </div>
146
+ <p className={cn("mt-1 line-clamp-1 text-[11px] leading-relaxed", tone.text)}>
147
+ {readSceneSummary(scene, visual, language)}
148
+ </p>
149
+ </div>
150
+ </button>
151
+ );
152
+ }
153
+
154
+ export function MarketplaceCuratedSceneView(props: {
155
+ scene: MarketplaceSceneView;
156
+ entries: MarketplaceShelfEntry[];
157
+ isLoading: boolean;
158
+ language: string;
159
+ localeFallbacks: string[];
160
+ installState: InstallState;
161
+ onBack: () => void;
162
+ onOpen: (entry: MarketplaceShelfEntry) => void;
163
+ onInstall: (item: MarketplaceItemSummary) => void;
164
+ }) {
165
+ const {
166
+ scene,
167
+ entries,
168
+ isLoading,
169
+ language,
170
+ localeFallbacks,
171
+ installState,
172
+ onBack,
173
+ onOpen,
174
+ onInstall,
175
+ } = props;
176
+ const visual = resolveSceneVisual(scene.scene);
177
+ const Icon = visual.icon;
178
+ const tone = MARKETPLACE_SHELF_TONE_STYLES[visual.tone];
179
+
180
+ return (
181
+ <section className="flex min-h-full flex-col">
182
+ <div className="mb-4 flex min-w-0 items-start gap-2.5">
183
+ <button
184
+ type="button"
185
+ onClick={onBack}
186
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
187
+ aria-label={readLocalized({ zh: "返回", en: "Back" }, language)}
188
+ >
189
+ <ArrowLeft className="h-4 w-4" />
190
+ </button>
191
+ <div className="min-w-0 flex-1">
192
+ <div className="flex min-w-0 items-center gap-2">
193
+ <span
194
+ className={cn(
195
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border",
196
+ tone.icon,
197
+ )}
198
+ >
199
+ <Icon className="h-4 w-4" />
200
+ </span>
201
+ <h3 className="min-w-0 flex-1 truncate text-[15px] font-semibold text-gray-950">
202
+ {readSceneTitle(scene, visual, language)}
203
+ </h3>
204
+ <span className="shrink-0 text-[11px] font-medium text-gray-400">
205
+ {readLocalized(
206
+ isLoading
207
+ ? { zh: "加载中", en: "Loading" }
208
+ : { zh: `${entries.length} 个技能`, en: `${entries.length} skills` },
209
+ language,
210
+ )}
211
+ </span>
212
+ </div>
213
+ <div className="mt-1.5 flex min-w-0 items-center gap-2">
214
+ <p className="min-w-0 flex-1 truncate text-[12px] leading-relaxed text-gray-500">
215
+ {readSceneSummary(scene, visual, language)}
216
+ </p>
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ {isLoading ? (
222
+ <div
223
+ data-testid="marketplace-scene-skeleton"
224
+ className={cn(
225
+ SCENE_CARD_GRID_CLASS,
226
+ "min-h-0 flex-1 auto-rows-[166px] content-start",
227
+ )}
228
+ >
229
+ <MarketplaceListSkeleton count={SCENE_SKELETON_CARD_COUNT} />
230
+ </div>
231
+ ) : entries.length > 0 ? (
232
+ <div className={SCENE_CARD_GRID_CLASS}>
233
+ {entries.map((entry) => (
234
+ <SkillShelfCard
235
+ key={entry.item.id}
236
+ entry={entry}
237
+ language={language}
238
+ localeFallbacks={localeFallbacks}
239
+ installState={installState}
240
+ layout="grid"
241
+ onOpen={onOpen}
242
+ onInstall={onInstall}
243
+ />
244
+ ))}
245
+ </div>
246
+ ) : (
247
+ <div className="rounded-lg border border-dashed border-gray-200 py-8 text-center text-[12px] text-gray-500">
248
+ {readLocalized({ zh: "这个模块暂无技能。", en: "No skills in this module yet." }, language)}
249
+ </div>
250
+ )}
251
+ </section>
252
+ );
253
+ }
254
+
255
+ function ShelfItemRow(props: {
256
+ icon: ComponentType<{ className?: string }>;
257
+ title: string;
258
+ description: string;
259
+ entries: MarketplaceShelfEntry[];
260
+ language: string;
261
+ localeFallbacks: string[];
262
+ installState: InstallState;
263
+ onOpen: (entry: MarketplaceShelfEntry) => void;
264
+ onInstall: (item: MarketplaceItemSummary) => void;
265
+ }) {
266
+ const {
267
+ icon: Icon,
268
+ title,
269
+ description,
270
+ entries,
271
+ language,
272
+ localeFallbacks,
273
+ installState,
274
+ onOpen,
275
+ onInstall,
276
+ } = props;
277
+
278
+ return (
279
+ <section className="space-y-2.5">
280
+ <ShelfHeader icon={Icon} title={title} description={description} />
281
+ <div className="-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 custom-scrollbar">
282
+ {entries.map((entry) => (
283
+ <SkillShelfCard
284
+ key={entry.item.id}
285
+ entry={entry}
286
+ language={language}
287
+ localeFallbacks={localeFallbacks}
288
+ installState={installState}
289
+ onOpen={onOpen}
290
+ onInstall={onInstall}
291
+ />
292
+ ))}
293
+ </div>
294
+ </section>
295
+ );
296
+ }
297
+
298
+ function ShelfHeader({
299
+ icon: Icon,
300
+ title,
301
+ description,
302
+ }: {
303
+ icon: ComponentType<{ className?: string }>;
304
+ title: string;
305
+ description: string;
306
+ }) {
307
+ return (
308
+ <div className="flex items-end justify-between gap-4">
309
+ <div>
310
+ <h3 className="flex items-center gap-2 text-[14px] font-semibold text-gray-950">
311
+ <Icon className="h-4 w-4 text-primary" />
312
+ {title}
313
+ </h3>
314
+ <p className="mt-0.5 text-[12px] leading-relaxed text-gray-500">
315
+ {description}
316
+ </p>
317
+ </div>
318
+ </div>
319
+ );
320
+ }
321
+
322
+ function compareUpdatedAt(left: MarketplaceItemSummary, right: MarketplaceItemSummary) {
323
+ const leftTs = Date.parse(left.updatedAt);
324
+ const rightTs = Date.parse(right.updatedAt);
325
+ if (Number.isNaN(leftTs) || Number.isNaN(rightTs)) {
326
+ return right.updatedAt.localeCompare(left.updatedAt);
327
+ }
328
+ return rightTs - leftTs;
329
+ }
330
+
331
+ function readLocalized(text: MarketplaceShelfLocalizedText, language: string) {
332
+ return language.startsWith("zh") ? text.zh : text.en;
333
+ }
334
+
335
+ function resolveSceneVisual(scene: string): MarketplaceShelfSceneVisual {
336
+ return MARKETPLACE_SHELF_SCENE_VISUALS.find((visual) => visual.scene === scene) ?? {
337
+ scene,
338
+ ...MARKETPLACE_SHELF_FALLBACK_VISUAL,
339
+ };
340
+ }
341
+
342
+ function readSceneTitle(scene: MarketplaceSceneView, visual: MarketplaceShelfSceneVisual, language: string) {
343
+ return visual.title ? readLocalized(visual.title, language) : scene.title;
344
+ }
345
+
346
+ function readSceneSummary(scene: MarketplaceSceneView, visual: MarketplaceShelfSceneVisual, language: string) {
347
+ if (visual.summary) {
348
+ return readLocalized(visual.summary, language);
349
+ }
350
+ return scene.description ?? scene.scene;
351
+ }
352
+
353
+ function readSceneCount(count: number, language: string) {
354
+ return language.startsWith("zh") ? `${count} 个技能` : `${count} skills`;
355
+ }
@@ -0,0 +1,118 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ } from "@/shared/lib/api";
5
+ import type { InstallState } from "@/features/marketplace/components/marketplace-list-card";
6
+ import { MarketplaceItemIcon } from "@/features/marketplace/components/marketplace-item-icon";
7
+ import { pickLocalizedText } from "@/features/marketplace/components/marketplace-localization";
8
+ import { t } from "@/shared/lib/i18n";
9
+ import { cn } from "@/shared/lib/utils";
10
+ import { CheckCircle2, Download } from "lucide-react";
11
+
12
+ type SkillShelfCardEntry = {
13
+ item: MarketplaceItemSummary;
14
+ record?: MarketplaceInstalledRecord;
15
+ };
16
+
17
+ export function SkillShelfCard(props: {
18
+ entry: SkillShelfCardEntry;
19
+ language: string;
20
+ localeFallbacks: string[];
21
+ installState: InstallState;
22
+ layout?: "rail" | "grid";
23
+ onOpen: (entry: SkillShelfCardEntry) => void;
24
+ onInstall: (item: MarketplaceItemSummary) => void;
25
+ }) {
26
+ const {
27
+ entry,
28
+ language,
29
+ localeFallbacks,
30
+ installState,
31
+ layout = "rail",
32
+ onOpen,
33
+ onInstall,
34
+ } = props;
35
+ const { item, record } = entry;
36
+ const summary = pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks);
37
+ const installSpec = item.install.spec;
38
+ const isInstalling = installState.installingSpecs.has(installSpec);
39
+ const isInstalled = Boolean(record);
40
+
41
+ return (
42
+ <article
43
+ onClick={() => onOpen(entry)}
44
+ className={cn(
45
+ "group flex min-h-[166px] cursor-pointer flex-col justify-between rounded-xl border border-gray-200/70 bg-white p-3 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50/60",
46
+ layout === "rail" ? "w-[260px] shrink-0" : "w-full min-w-0",
47
+ )}
48
+ >
49
+ <div>
50
+ <div className="mb-2.5 flex min-w-0 items-start gap-2.5">
51
+ <MarketplaceItemIcon name={item.name} fallback={item.install.spec} />
52
+ <div className="min-w-0 flex-1 pt-0.5">
53
+ <div className="truncate text-[13px] font-semibold leading-tight text-gray-950">
54
+ {item.name}
55
+ </div>
56
+ <div className="mt-0.5 truncate text-[11px] font-mono leading-tight text-gray-400">
57
+ {formatShelfMeta(item)}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <p className="line-clamp-2 text-[12px] leading-relaxed text-gray-500">
62
+ {summary}
63
+ </p>
64
+ <TagLine tags={item.tags} />
65
+ </div>
66
+
67
+ <div className="mt-3 flex items-center justify-between gap-3 border-t border-gray-100 pt-2.5">
68
+ <span className="min-w-0 truncate text-[11px] text-gray-400">
69
+ {formatUpdatedAt(item.updatedAt)}
70
+ </span>
71
+ {isInstalled ? (
72
+ <span className="inline-flex h-7 items-center gap-1.5 text-[11px] font-semibold text-emerald-700">
73
+ <CheckCircle2 className="h-3.5 w-3.5" />
74
+ {readLocalized({ zh: "已安装", en: "Installed" }, language)}
75
+ </span>
76
+ ) : (
77
+ <button
78
+ type="button"
79
+ disabled={isInstalling}
80
+ onClick={(event) => {
81
+ event.stopPropagation();
82
+ onInstall(item);
83
+ }}
84
+ className="inline-flex h-7 items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2 text-[11px] font-semibold text-gray-700 transition-colors hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50"
85
+ >
86
+ <Download className="h-3.5 w-3.5" />
87
+ {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
88
+ </button>
89
+ )}
90
+ </div>
91
+ </article>
92
+ );
93
+ }
94
+
95
+ function formatShelfMeta(item: MarketplaceItemSummary) {
96
+ return item.slug || item.install.spec;
97
+ }
98
+
99
+ function TagLine({ tags }: { tags: string[] }) {
100
+ const visibleTags = tags.slice(0, 2);
101
+ if (visibleTags.length === 0) {
102
+ return null;
103
+ }
104
+ return (
105
+ <div className="mt-2 truncate text-[11px] font-medium text-gray-400">
106
+ {visibleTags.join(" / ")}
107
+ </div>
108
+ );
109
+ }
110
+
111
+ function formatUpdatedAt(value: string) {
112
+ const date = value.slice(0, 10);
113
+ return date || value;
114
+ }
115
+
116
+ function readLocalized(text: { zh: string; en: string }, language: string) {
117
+ return language.startsWith("zh") ? text.zh : text.en;
118
+ }
@@ -0,0 +1,201 @@
1
+ export function escapeHtml(text: string): string {
2
+ return text
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;")
7
+ .replace(/'/g, "&#39;");
8
+ }
9
+
10
+ function stripYamlFrontmatter(text: string): string {
11
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
12
+ if (lines[0]?.trim() !== "---") {
13
+ return text;
14
+ }
15
+ const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
16
+ return closingIndex > 0 ? lines.slice(closingIndex + 1).join("\n").trim() : text;
17
+ }
18
+
19
+ function renderInlineMarkdown(text: string): string {
20
+ return escapeHtml(text)
21
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
22
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
23
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
24
+ }
25
+
26
+ function isMarkdownBlockStart(line: string): boolean {
27
+ return /^(#{1,4})\s+/.test(line)
28
+ || /^([-*])\s+/.test(line)
29
+ || /^\d+\.\s+/.test(line)
30
+ || /^>\s?/.test(line)
31
+ || /^```/.test(line);
32
+ }
33
+
34
+ type MarkdownRenderResult = {
35
+ html: string;
36
+ nextIndex: number;
37
+ };
38
+
39
+ function renderCodeBlock(lines: string[], index: number, fenceMatch: RegExpMatchArray): MarkdownRenderResult {
40
+ const codeLines: string[] = [];
41
+ let nextIndex = index + 1;
42
+ while (nextIndex < lines.length && !lines[nextIndex]?.trim().startsWith("```")) {
43
+ codeLines.push(lines[nextIndex] ?? "");
44
+ nextIndex += 1;
45
+ }
46
+ const language = fenceMatch[1] ? `<span class="code-language">${escapeHtml(fenceMatch[1])}</span>` : "";
47
+ return {
48
+ html: `<div class="code-block">${language}<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre></div>`,
49
+ nextIndex: nextIndex + 1,
50
+ };
51
+ }
52
+
53
+ function renderHeadingBlock(headingMatch: RegExpMatchArray, index: number): MarkdownRenderResult {
54
+ const level = Math.min(4, headingMatch[1]?.length ?? 2);
55
+ return {
56
+ html: `<h${level}>${renderInlineMarkdown(headingMatch[2] ?? "")}</h${level}>`,
57
+ nextIndex: index + 1,
58
+ };
59
+ }
60
+
61
+ function renderListBlock(lines: string[], index: number, ordered: boolean): MarkdownRenderResult {
62
+ const items: string[] = [];
63
+ let nextIndex = index;
64
+ const matcher = ordered ? /^\d+\.\s+/ : /^[-*]\s+/;
65
+ while (nextIndex < lines.length && matcher.test(lines[nextIndex]?.trim() ?? "")) {
66
+ items.push(`<li>${renderInlineMarkdown((lines[nextIndex] ?? "").trim().replace(matcher, ""))}</li>`);
67
+ nextIndex += 1;
68
+ }
69
+ return {
70
+ html: ordered ? `<ol>${items.join("")}</ol>` : `<ul>${items.join("")}</ul>`,
71
+ nextIndex,
72
+ };
73
+ }
74
+
75
+ function renderQuoteBlock(lines: string[], index: number): MarkdownRenderResult {
76
+ const quotes: string[] = [];
77
+ let nextIndex = index;
78
+ while (nextIndex < lines.length && /^>\s?/.test(lines[nextIndex]?.trim() ?? "")) {
79
+ quotes.push((lines[nextIndex] ?? "").trim().replace(/^>\s?/, ""));
80
+ nextIndex += 1;
81
+ }
82
+ return {
83
+ html: `<blockquote>${renderInlineMarkdown(quotes.join(" "))}</blockquote>`,
84
+ nextIndex,
85
+ };
86
+ }
87
+
88
+ function renderParagraphBlock(lines: string[], index: number): MarkdownRenderResult {
89
+ const paragraphLines: string[] = [];
90
+ let nextIndex = index;
91
+ while (nextIndex < lines.length) {
92
+ const paragraphLine = lines[nextIndex]?.trim() ?? "";
93
+ if (!paragraphLine || isMarkdownBlockStart(paragraphLine)) {
94
+ break;
95
+ }
96
+ paragraphLines.push(paragraphLine);
97
+ nextIndex += 1;
98
+ }
99
+ return {
100
+ html: `<p>${renderInlineMarkdown(paragraphLines.join(" "))}</p>`,
101
+ nextIndex,
102
+ };
103
+ }
104
+
105
+ function renderMarkdownBlock(lines: string[], index: number): MarkdownRenderResult {
106
+ const trimmed = lines[index]?.trim() ?? "";
107
+ const fenceMatch = trimmed.match(/^```(\S*)/);
108
+ if (fenceMatch) {
109
+ return renderCodeBlock(lines, index, fenceMatch);
110
+ }
111
+
112
+ const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/);
113
+ if (headingMatch) {
114
+ return renderHeadingBlock(headingMatch, index);
115
+ }
116
+
117
+ if (/^[-*]\s+/.test(trimmed)) {
118
+ return renderListBlock(lines, index, false);
119
+ }
120
+ if (/^\d+\.\s+/.test(trimmed)) {
121
+ return renderListBlock(lines, index, true);
122
+ }
123
+ if (/^>\s?/.test(trimmed)) {
124
+ return renderQuoteBlock(lines, index);
125
+ }
126
+ return renderParagraphBlock(lines, index);
127
+ }
128
+
129
+ export function renderDetailMarkdown(markdown: string): string {
130
+ const lines = stripYamlFrontmatter(markdown).replace(/\r\n/g, "\n").split("\n");
131
+ const blocks: string[] = [];
132
+ let index = 0;
133
+
134
+ while (index < lines.length) {
135
+ if (!(lines[index] ?? "").trim()) {
136
+ index += 1;
137
+ continue;
138
+ }
139
+
140
+ const result = renderMarkdownBlock(lines, index);
141
+ blocks.push(result.html);
142
+ index = result.nextIndex;
143
+ }
144
+
145
+ return blocks.join("");
146
+ }
147
+
148
+ type MetadataEntry = {
149
+ key: string;
150
+ value: string;
151
+ };
152
+
153
+ function stringifyMetadataValue(value: unknown): string {
154
+ if (Array.isArray(value)) {
155
+ return value.map((entry) => stringifyMetadataValue(entry)).join(", ");
156
+ }
157
+ if (value && typeof value === "object") {
158
+ return JSON.stringify(value);
159
+ }
160
+ return String(value ?? "");
161
+ }
162
+
163
+ function readJsonMetadata(raw: string): MetadataEntry[] {
164
+ try {
165
+ const parsed = JSON.parse(raw) as unknown;
166
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
167
+ return [];
168
+ }
169
+ return Object.entries(parsed)
170
+ .map(([key, value]) => ({ key, value: stringifyMetadataValue(value) }))
171
+ .filter((entry) => entry.value.trim().length > 0);
172
+ } catch {
173
+ return [];
174
+ }
175
+ }
176
+
177
+ function readYamlLikeMetadata(raw: string): MetadataEntry[] {
178
+ return raw
179
+ .replace(/\r\n/g, "\n")
180
+ .split("\n")
181
+ .map((line) => line.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/))
182
+ .filter((match): match is RegExpMatchArray => Boolean(match))
183
+ .map((match) => ({
184
+ key: match[1] ?? "",
185
+ value: match[2]?.replace(/^["']|["']$/g, "") ?? "",
186
+ }))
187
+ .filter((entry) => entry.key && entry.value.trim().length > 0);
188
+ }
189
+
190
+ export function renderDetailMetadata(raw: string): string {
191
+ const entries = readJsonMetadata(raw);
192
+ const metadataEntries = entries.length > 0 ? entries : readYamlLikeMetadata(raw);
193
+ if (metadataEntries.length === 0) {
194
+ return `<pre class="code">${escapeHtml(raw)}</pre>`;
195
+ }
196
+ return `<dl class="metadata-list">${metadataEntries
197
+ .map(
198
+ (entry) => `<div><dt>${escapeHtml(entry.key)}</dt><dd>${escapeHtml(entry.value)}</dd></div>`,
199
+ )
200
+ .join("")}</dl>`;
201
+ }