@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
@@ -0,0 +1,143 @@
1
+ import { useState } from 'react';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useRuntimeControl } from '@/hooks/use-runtime-control';
4
+ import { runtimeControlManager } from '@/runtime-control/runtime-control.manager';
5
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
6
+ import { t } from '@/lib/i18n';
7
+ import { cn } from '@/lib/utils';
8
+ import { toast } from 'sonner';
9
+
10
+ type RuntimeStatusTone = 'healthy' | 'attention' | 'inactive';
11
+
12
+ const runtimeStatusToneStyles: Record<RuntimeStatusTone, string> = {
13
+ healthy: 'bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.14)]',
14
+ attention: 'bg-amber-400 shadow-[0_0_0_3px_rgba(251,191,36,0.16)]',
15
+ inactive: 'bg-gray-300 shadow-[0_0_0_3px_rgba(156,163,175,0.12)]'
16
+ };
17
+
18
+ type RuntimeStatusSummary = {
19
+ actionLabel: string | null;
20
+ description: string;
21
+ reasonLines: string[];
22
+ title: string;
23
+ tone: RuntimeStatusTone;
24
+ };
25
+
26
+ function buildRuntimeStatusSummary(
27
+ view: ReturnType<typeof useRuntimeControl>['data']
28
+ ): RuntimeStatusSummary {
29
+ if (!view) {
30
+ return {
31
+ tone: 'inactive',
32
+ title: t('runtimeStatusLoadingTitle'),
33
+ description: t('runtimeStatusLoadingDescription'),
34
+ reasonLines: [],
35
+ actionLabel: null
36
+ };
37
+ }
38
+
39
+ if (view.pendingRestart) {
40
+ return {
41
+ tone: 'attention',
42
+ title: t('runtimeStatusPendingRestartTitle'),
43
+ description: t('runtimeStatusPendingRestartDescription'),
44
+ reasonLines:
45
+ view.pendingRestart.changedPaths.length > 0
46
+ ? view.pendingRestart.changedPaths.map((path) =>
47
+ t('runtimeStatusPendingRestartReasonItem').replace('{path}', path)
48
+ )
49
+ : [view.pendingRestart.message],
50
+ actionLabel: view.canRestartService.available ? t('runtimeStatusRestartAction') : null
51
+ };
52
+ }
53
+
54
+ return {
55
+ tone: view.lifecycle === 'healthy' ? 'healthy' : 'inactive',
56
+ title: t('runtimeStatusHealthyTitle'),
57
+ description: t('runtimeStatusHealthyDescription'),
58
+ reasonLines: [],
59
+ actionLabel: null
60
+ };
61
+ }
62
+
63
+ export function RuntimeStatusEntry() {
64
+ const queryClient = useQueryClient();
65
+ const runtimeControlQuery = useRuntimeControl();
66
+ const [isRestarting, setIsRestarting] = useState(false);
67
+ const runtimeView = runtimeControlQuery.data;
68
+ const summary = buildRuntimeStatusSummary(runtimeView);
69
+ const title = runtimeControlQuery.isError ? t('runtimeControlLoadFailed') : summary.title;
70
+ const description =
71
+ runtimeControlQuery.isError && runtimeControlQuery.error instanceof Error
72
+ ? runtimeControlQuery.error.message
73
+ : summary.description;
74
+ const canRestart = Boolean(runtimeView?.pendingRestart && runtimeView.canRestartService.available);
75
+
76
+ const handleRestart = async () => {
77
+ if (!canRestart) {
78
+ return;
79
+ }
80
+ setIsRestarting(true);
81
+ try {
82
+ const result = await runtimeControlManager.controlService('restart-service');
83
+ toast.success(result.message);
84
+ await queryClient.invalidateQueries({ queryKey: ['runtime-control'] });
85
+ } catch (error) {
86
+ const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
87
+ toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
88
+ } finally {
89
+ setIsRestarting(false);
90
+ }
91
+ };
92
+
93
+ return (
94
+ <Popover>
95
+ <PopoverTrigger asChild>
96
+ <button
97
+ type="button"
98
+ className="inline-flex items-center justify-center rounded-full p-0.5 transition-transform hover:scale-105"
99
+ aria-label={title}
100
+ title={title}
101
+ data-testid="runtime-status-entry"
102
+ >
103
+ <span className={cn('h-2.5 w-2.5 rounded-full', runtimeStatusToneStyles[summary.tone])} />
104
+ </button>
105
+ </PopoverTrigger>
106
+ <PopoverContent
107
+ align="start"
108
+ sideOffset={10}
109
+ className="w-[290px] space-y-3 rounded-2xl border border-gray-200 bg-white p-4"
110
+ >
111
+ <div className="space-y-1">
112
+ <div className="text-sm font-semibold text-gray-900">{title}</div>
113
+ <p className="text-xs leading-5 text-gray-600">{description}</p>
114
+ </div>
115
+ {summary.reasonLines.length > 0 ? (
116
+ <div className="space-y-2">
117
+ {summary.reasonLines.map((reason) => (
118
+ <div
119
+ key={reason}
120
+ className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-900"
121
+ >
122
+ {reason}
123
+ </div>
124
+ ))}
125
+ </div>
126
+ ) : null}
127
+ {summary.actionLabel ? (
128
+ <div className="flex items-center justify-between border-t border-gray-100 pt-1">
129
+ <span className="text-[11px] text-gray-500">{t('runtimeStatusActionHint')}</span>
130
+ <button
131
+ type="button"
132
+ onClick={() => void handleRestart()}
133
+ disabled={isRestarting}
134
+ className="text-sm font-semibold text-sky-600 transition-colors hover:text-sky-700 disabled:text-gray-400"
135
+ >
136
+ {isRestarting ? t('runtimeStatusRestartingAction') : summary.actionLabel}
137
+ </button>
138
+ </div>
139
+ ) : null}
140
+ </PopoverContent>
141
+ </Popover>
142
+ );
143
+ }
@@ -0,0 +1,93 @@
1
+ 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
+ export function buildGenericDetailDataUrl(params: {
11
+ title: string;
12
+ typeLabel: string;
13
+ spec: string;
14
+ summary?: string;
15
+ description?: string;
16
+ metadataRaw?: string;
17
+ contentRaw?: string;
18
+ sourceUrl?: string;
19
+ sourceLabel?: string;
20
+ tags?: string[];
21
+ author?: string;
22
+ }): string {
23
+ const {
24
+ title,
25
+ typeLabel,
26
+ spec,
27
+ summary: rawSummary,
28
+ description: rawDescription,
29
+ metadataRaw,
30
+ contentRaw,
31
+ sourceUrl,
32
+ sourceLabel,
33
+ tags,
34
+ author,
35
+ } = params;
36
+ const metadata = metadataRaw?.trim() || "-";
37
+ const content = contentRaw?.trim() || "-";
38
+ const summary = rawSummary?.trim();
39
+ const description = rawDescription?.trim();
40
+ const shouldShowDescription = Boolean(description) && description !== summary;
41
+
42
+ const html = `<!doctype html>
43
+ <html>
44
+ <head>
45
+ <meta charset="utf-8" />
46
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
47
+ <title>${escapeHtml(title)}</title>
48
+ <style>
49
+ :root { color-scheme: light; }
50
+ body { margin: 0; background: #f7f9fc; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
51
+ .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 40px; }
52
+ .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); }
53
+ .hero h1 { margin: 0; font-size: 26px; }
54
+ .meta { margin-top: 8px; color: #475569; font-size: 13px; overflow-wrap: anywhere; word-break: break-word; }
55
+ .summary { margin-top: 14px; font-size: 14px; line-height: 1.7; color: #334155; }
56
+ .grid { display: grid; grid-template-columns: 260px 1fr; gap: 14px; margin-top: 16px; }
57
+ .card { border: 1px solid #e2e8f0; background: #fff; border-radius: 14px; overflow: hidden; }
58
+ .card h2 { margin: 0; padding: 12px 14px; font-size: 13px; font-weight: 700; color: #1d4ed8; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
59
+ .card .body { padding: 12px 14px; font-size: 13px; color: #334155; line-height: 1.7; }
60
+ .code { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; margin: 0; }
61
+ .tags { margin-top: 10px; }
62
+ .tag { display: inline-block; margin: 0 6px 6px 0; padding: 4px 9px; border-radius: 999px; background: #e0e7ff; color: #3730a3; font-size: 11px; }
63
+ .source { color: #2563eb; text-decoration: none; overflow-wrap: anywhere; word-break: break-all; }
64
+ @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <main class="wrap">
69
+ <section class="hero">
70
+ <h1>${escapeHtml(title)}</h1>
71
+ <div class="meta">${escapeHtml(typeLabel)} · ${escapeHtml(spec)}${author ? ` · ${escapeHtml(author)}` : ""}</div>
72
+ ${summary ? `<p class="summary">${escapeHtml(summary)}</p>` : ""}
73
+ ${shouldShowDescription ? `<p class="summary">${escapeHtml(description as string)}</p>` : ""}
74
+ ${tags && tags.length > 0 ? `<div class="tags">${tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</div>` : ""}
75
+ ${sourceUrl ? `<p class="meta" style="margin-top:12px;">${escapeHtml(sourceLabel ?? "Source")}: <a class="source" href="${escapeHtml(sourceUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(sourceUrl)}</a></p>` : ""}
76
+ </section>
77
+
78
+ <section class="grid">
79
+ <article class="card">
80
+ <h2>Metadata</h2>
81
+ <div class="body"><pre class="code">${escapeHtml(metadata)}</pre></div>
82
+ </article>
83
+ <article class="card">
84
+ <h2>Content</h2>
85
+ <div class="body"><pre class="code">${escapeHtml(content)}</pre></div>
86
+ </article>
87
+ </section>
88
+ </main>
89
+ </body>
90
+ </html>`;
91
+
92
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
93
+ }
@@ -0,0 +1,288 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ MarketplaceManageAction,
5
+ } from "@/api/types";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from "@/components/ui/tooltip";
13
+ import { t } from "@/lib/i18n";
14
+ import { cn } from "@/lib/utils";
15
+ import {
16
+ buildLocaleFallbacks,
17
+ pickLocalizedText,
18
+ } from "@/components/marketplace/marketplace-localization";
19
+
20
+ export type InstallState = {
21
+ installingSpecs: ReadonlySet<string>;
22
+ };
23
+
24
+ export type ManageState = {
25
+ actionsByTarget: ReadonlyMap<string, MarketplaceManageAction>;
26
+ };
27
+
28
+ const ITEM_ICON_COLORS = [
29
+ "bg-amber-600",
30
+ "bg-orange-500",
31
+ "bg-yellow-600",
32
+ "bg-emerald-600",
33
+ "bg-teal-600",
34
+ "bg-cyan-600",
35
+ "bg-stone-600",
36
+ "bg-rose-500",
37
+ "bg-violet-500",
38
+ ] as const;
39
+
40
+ function getAvatarColor(text: string) {
41
+ let hash = 0;
42
+ for (let i = 0; i < text.length; i++) {
43
+ hash = text.charCodeAt(i) + ((hash << 5) - hash);
44
+ }
45
+ return ITEM_ICON_COLORS[Math.abs(hash) % ITEM_ICON_COLORS.length];
46
+ }
47
+
48
+ function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
49
+ const displayName = name || fallback;
50
+ const letters = displayName.substring(0, 2).toUpperCase();
51
+ const colorClass = getAvatarColor(displayName);
52
+ return (
53
+ <div
54
+ className={cn(
55
+ "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-sm font-semibold text-white",
56
+ colorClass,
57
+ )}
58
+ >
59
+ {letters}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ function MarketplaceListCardMeta(props: {
65
+ title: string;
66
+ spec: string;
67
+ summary: string;
68
+ }) {
69
+ const { title, spec, summary } = props;
70
+
71
+ return (
72
+ <TooltipProvider delayDuration={400}>
73
+ <Tooltip>
74
+ <TooltipTrigger asChild>
75
+ <div className="truncate text-[14px] font-semibold leading-tight text-gray-900">
76
+ {title}
77
+ </div>
78
+ </TooltipTrigger>
79
+ <TooltipContent className="max-w-[300px] text-xs">
80
+ {title}
81
+ </TooltipContent>
82
+ </Tooltip>
83
+
84
+ <div className="mb-1.5 mt-0.5 flex items-center gap-1.5">
85
+ {spec ? (
86
+ <Tooltip>
87
+ <TooltipTrigger asChild>
88
+ <span className="max-w-full truncate font-mono text-[11px] text-gray-400">
89
+ {spec}
90
+ </span>
91
+ </TooltipTrigger>
92
+ <TooltipContent className="max-w-[300px] break-all font-mono text-xs">
93
+ {spec}
94
+ </TooltipContent>
95
+ </Tooltip>
96
+ ) : null}
97
+ </div>
98
+
99
+ <Tooltip>
100
+ <TooltipTrigger asChild>
101
+ <p className="line-clamp-1 text-left text-[12px] leading-relaxed text-gray-500/90 transition-colors">
102
+ {summary}
103
+ </p>
104
+ </TooltipTrigger>
105
+ {summary ? (
106
+ <TooltipContent className="max-w-[400px] text-xs leading-relaxed">
107
+ {summary}
108
+ </TooltipContent>
109
+ ) : null}
110
+ </Tooltip>
111
+ </TooltipProvider>
112
+ );
113
+ }
114
+
115
+ function readToggleLabel(
116
+ busyAction: MarketplaceManageAction | undefined,
117
+ isDisabled: boolean,
118
+ ) {
119
+ if (busyAction && busyAction !== "uninstall") {
120
+ return busyAction === "enable"
121
+ ? t("marketplaceEnabling")
122
+ : t("marketplaceDisabling");
123
+ }
124
+ return isDisabled ? t("marketplaceEnable") : t("marketplaceDisable");
125
+ }
126
+
127
+ function MarketplaceListCardActions(props: {
128
+ item?: MarketplaceItemSummary;
129
+ record?: MarketplaceInstalledRecord;
130
+ isInstalling: boolean;
131
+ busyAction: MarketplaceManageAction | undefined;
132
+ busyForRecord: boolean;
133
+ isDisabled: boolean;
134
+ onInstall: (item: MarketplaceItemSummary) => void;
135
+ onManage: (
136
+ action: MarketplaceManageAction,
137
+ record: MarketplaceInstalledRecord,
138
+ ) => void;
139
+ }) {
140
+ const {
141
+ item,
142
+ record,
143
+ isInstalling,
144
+ busyAction,
145
+ busyForRecord,
146
+ isDisabled,
147
+ onInstall,
148
+ onManage,
149
+ } = props;
150
+ const pluginRecord = record?.type === "plugin" ? record : undefined;
151
+ const canUninstall =
152
+ (record?.type === "plugin" && record.origin !== "bundled") ||
153
+ (record?.type === "skill" && record.source === "workspace");
154
+
155
+ return (
156
+ <div className="flex h-full shrink-0 items-center">
157
+ {item && !record ? (
158
+ <Button
159
+ type="button"
160
+ size="sm"
161
+ variant="primary"
162
+ onClick={(event) => {
163
+ event.stopPropagation();
164
+ onInstall(item);
165
+ }}
166
+ disabled={isInstalling}
167
+ className="rounded-xl"
168
+ >
169
+ {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
170
+ </Button>
171
+ ) : null}
172
+
173
+ {pluginRecord ? (
174
+ <Button
175
+ type="button"
176
+ size="sm"
177
+ variant="outline"
178
+ disabled={busyForRecord}
179
+ onClick={(event) => {
180
+ event.stopPropagation();
181
+ onManage(isDisabled ? "enable" : "disable", pluginRecord);
182
+ }}
183
+ className="rounded-xl border-gray-200/80 text-gray-600"
184
+ >
185
+ {readToggleLabel(busyAction, isDisabled)}
186
+ </Button>
187
+ ) : null}
188
+
189
+ {record && canUninstall ? (
190
+ <Button
191
+ type="button"
192
+ size="sm"
193
+ variant="outline"
194
+ disabled={busyForRecord}
195
+ onClick={(event) => {
196
+ event.stopPropagation();
197
+ onManage("uninstall", record);
198
+ }}
199
+ className="rounded-xl border-rose-100 text-rose-500 hover:border-rose-200 hover:bg-rose-50 hover:text-rose-600"
200
+ >
201
+ {busyAction === "uninstall"
202
+ ? t("marketplaceRemoving")
203
+ : t("marketplaceUninstall")}
204
+ </Button>
205
+ ) : null}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ export function MarketplaceListCard(props: {
211
+ item?: MarketplaceItemSummary;
212
+ record?: MarketplaceInstalledRecord;
213
+ language: string;
214
+ installState: InstallState;
215
+ manageState: ManageState;
216
+ onOpen: () => void;
217
+ onInstall: (item: MarketplaceItemSummary) => void;
218
+ onManage: (
219
+ action: MarketplaceManageAction,
220
+ record: MarketplaceInstalledRecord,
221
+ ) => void;
222
+ }) {
223
+ const {
224
+ item,
225
+ record,
226
+ language,
227
+ installState,
228
+ manageState,
229
+ onOpen,
230
+ onInstall,
231
+ onManage,
232
+ } = props;
233
+ const localeFallbacks = buildLocaleFallbacks(language);
234
+ const title =
235
+ item?.name ??
236
+ record?.label ??
237
+ record?.id ??
238
+ record?.spec ??
239
+ t("marketplaceUnknownItem");
240
+ const summary =
241
+ pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks) ||
242
+ (record ? t("marketplaceInstalledLocalSummary") : "");
243
+ const spec = item?.install.spec ?? record?.spec ?? "";
244
+ const targetId = record?.id || record?.spec;
245
+ const busyAction = targetId
246
+ ? manageState.actionsByTarget.get(targetId)
247
+ : undefined;
248
+ const busyForRecord = Boolean(busyAction);
249
+ const isDisabled = record
250
+ ? record.enabled === false || record.runtimeStatus === "disabled"
251
+ : false;
252
+ const installSpec = item?.install.spec;
253
+ const isInstalling =
254
+ typeof installSpec === "string" &&
255
+ installState.installingSpecs.has(installSpec);
256
+
257
+ return (
258
+ <article
259
+ onClick={onOpen}
260
+ className="group flex cursor-pointer items-start justify-between gap-3.5 rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm transition-all hover:border-blue-300/80 hover:shadow-md"
261
+ >
262
+ <div className="flex h-full min-w-0 flex-1 items-start gap-3">
263
+ <ItemIcon
264
+ name={title}
265
+ fallback={spec || t("marketplaceTypeExtension")}
266
+ />
267
+ <div className="flex h-full min-w-0 flex-1 flex-col justify-center">
268
+ <MarketplaceListCardMeta
269
+ title={title}
270
+ spec={spec}
271
+ summary={summary}
272
+ />
273
+ </div>
274
+ </div>
275
+
276
+ <MarketplaceListCardActions
277
+ item={item}
278
+ record={record}
279
+ isInstalling={isInstalling}
280
+ busyAction={busyAction}
281
+ busyForRecord={busyForRecord}
282
+ isDisabled={isDisabled}
283
+ onInstall={onInstall}
284
+ onManage={onManage}
285
+ />
286
+ </article>
287
+ );
288
+ }
@@ -0,0 +1,129 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ } from "@/api/types";
5
+ import { pickLocalizedText } from "@/components/marketplace/marketplace-localization";
6
+
7
+ export type InstalledRenderEntry = {
8
+ key: string;
9
+ record: MarketplaceInstalledRecord;
10
+ item?: MarketplaceItemSummary;
11
+ };
12
+
13
+ function normalizeMarketplaceKey(value: string | undefined): string {
14
+ return (value ?? "").trim().toLowerCase();
15
+ }
16
+
17
+ function toLookupKey(
18
+ type: MarketplaceItemSummary["type"],
19
+ value: string | undefined,
20
+ ): string {
21
+ const normalized = normalizeMarketplaceKey(value);
22
+ return normalized.length > 0 ? `${type}:${normalized}` : "";
23
+ }
24
+
25
+ export function buildCatalogLookup(
26
+ items: MarketplaceItemSummary[],
27
+ ): Map<string, MarketplaceItemSummary> {
28
+ const lookup = new Map<string, MarketplaceItemSummary>();
29
+
30
+ for (const item of items) {
31
+ const candidates = [item.install.spec, item.slug, item.id];
32
+ for (const candidate of candidates) {
33
+ const lookupKey = toLookupKey(item.type, candidate);
34
+ if (!lookupKey || lookup.has(lookupKey)) {
35
+ continue;
36
+ }
37
+ lookup.set(lookupKey, item);
38
+ }
39
+ }
40
+
41
+ return lookup;
42
+ }
43
+
44
+ export function buildInstalledRecordLookup(
45
+ records: MarketplaceInstalledRecord[],
46
+ ): Map<string, MarketplaceInstalledRecord> {
47
+ const lookup = new Map<string, MarketplaceInstalledRecord>();
48
+
49
+ for (const record of records) {
50
+ const candidates = [record.spec, record.id, record.label];
51
+ for (const candidate of candidates) {
52
+ const lookupKey = toLookupKey(record.type, candidate);
53
+ if (!lookupKey || lookup.has(lookupKey)) {
54
+ continue;
55
+ }
56
+ lookup.set(lookupKey, record);
57
+ }
58
+ }
59
+
60
+ return lookup;
61
+ }
62
+
63
+ export function findInstalledRecordForItem(
64
+ item: MarketplaceItemSummary,
65
+ installedRecordLookup: Map<string, MarketplaceInstalledRecord>,
66
+ ): MarketplaceInstalledRecord | undefined {
67
+ const candidates = [item.install.spec, item.slug, item.id];
68
+ for (const candidate of candidates) {
69
+ const lookupKey = toLookupKey(item.type, candidate);
70
+ if (!lookupKey) {
71
+ continue;
72
+ }
73
+ const record = installedRecordLookup.get(lookupKey);
74
+ if (record) {
75
+ return record;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ export function findCatalogItemForRecord(
82
+ record: MarketplaceInstalledRecord,
83
+ catalogLookup: Map<string, MarketplaceItemSummary>,
84
+ ): MarketplaceItemSummary | undefined {
85
+ const bySpec = catalogLookup.get(toLookupKey(record.type, record.spec));
86
+ if (bySpec) {
87
+ return bySpec;
88
+ }
89
+
90
+ const byId = catalogLookup.get(toLookupKey(record.type, record.id));
91
+ if (byId) {
92
+ return byId;
93
+ }
94
+
95
+ return catalogLookup.get(toLookupKey(record.type, record.label));
96
+ }
97
+
98
+ export function matchInstalledSearch(
99
+ record: MarketplaceInstalledRecord,
100
+ item: MarketplaceItemSummary | undefined,
101
+ query: string,
102
+ localeFallbacks: string[],
103
+ ): boolean {
104
+ const normalizedQuery = normalizeMarketplaceKey(query);
105
+ if (!normalizedQuery) {
106
+ return true;
107
+ }
108
+
109
+ const localizedSummary = pickLocalizedText(
110
+ item?.summaryI18n,
111
+ item?.summary,
112
+ localeFallbacks,
113
+ );
114
+ const values = [
115
+ record.id,
116
+ record.spec,
117
+ record.label,
118
+ item?.name,
119
+ item?.slug,
120
+ item?.summary,
121
+ localizedSummary,
122
+ ...(item?.tags ?? []),
123
+ ];
124
+
125
+ return values
126
+ .map((value) => normalizeMarketplaceKey(value))
127
+ .filter(Boolean)
128
+ .some((value) => value.includes(normalizedQuery));
129
+ }