@nextclaw/ui 0.15.1 → 0.15.3

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 (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -7,6 +7,9 @@ export type ChatSessionListSnapshot = {
7
7
  selectedAgentId: string;
8
8
  query: string;
9
9
  listMode: ChatSessionListMode;
10
+ pinnedSessionKeys: string[];
11
+ pinnedProjectRoots: string[];
12
+ collapsedProjectRoots: string[];
10
13
  };
11
14
 
12
15
  const CHAT_SESSION_LIST_MODE_STORAGE_KEY = 'nextclaw.chat.session-list.mode';
@@ -15,6 +18,9 @@ const CHAT_SESSION_LIST_DEFAULT_MODE: ChatSessionListMode = 'time-first';
15
18
  type PersistedChatSessionListStore = {
16
19
  snapshot?: {
17
20
  listMode?: unknown;
21
+ pinnedSessionKeys?: unknown;
22
+ pinnedProjectRoots?: unknown;
23
+ collapsedProjectRoots?: unknown;
18
24
  };
19
25
  };
20
26
 
@@ -22,6 +28,13 @@ function isChatSessionListMode(value: unknown): value is ChatSessionListMode {
22
28
  return value === 'time-first' || value === 'project-first';
23
29
  }
24
30
 
31
+ function normalizePersistedStringList(value: unknown): string[] {
32
+ if (!Array.isArray(value)) {
33
+ return [];
34
+ }
35
+ return [...new Set(value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter(Boolean))];
36
+ }
37
+
25
38
  function resolvePersistedChatSessionListMode(persistedState: unknown): ChatSessionListMode | null {
26
39
  if (!persistedState || typeof persistedState !== 'object') {
27
40
  return null;
@@ -75,7 +88,10 @@ const initialSnapshot: ChatSessionListSnapshot = {
75
88
  selectedSessionKey: null,
76
89
  selectedAgentId: 'main',
77
90
  query: '',
78
- listMode: CHAT_SESSION_LIST_DEFAULT_MODE
91
+ listMode: CHAT_SESSION_LIST_DEFAULT_MODE,
92
+ pinnedSessionKeys: [],
93
+ pinnedProjectRoots: [],
94
+ collapsedProjectRoots: [],
79
95
  };
80
96
 
81
97
  function createSetSnapshotAction(set: ChatSessionListStoreSet) {
@@ -123,19 +139,26 @@ export const useChatSessionListStore = create<ChatSessionListStore>()(
123
139
  storage: createJSONStorage(() => window.localStorage),
124
140
  partialize: (state): PersistedChatSessionListStore => ({
125
141
  snapshot: {
126
- listMode: state.snapshot.listMode
142
+ listMode: state.snapshot.listMode,
143
+ pinnedSessionKeys: state.snapshot.pinnedSessionKeys,
144
+ pinnedProjectRoots: state.snapshot.pinnedProjectRoots,
145
+ collapsedProjectRoots: state.snapshot.collapsedProjectRoots,
127
146
  }
128
147
  }),
129
148
  merge: (persistedState, currentState) => {
130
- const listMode = resolvePersistedChatSessionListMode(persistedState);
131
- if (!listMode) {
149
+ if (!persistedState || typeof persistedState !== 'object') {
132
150
  return currentState;
133
151
  }
152
+ const { snapshot } = persistedState as PersistedChatSessionListStore;
153
+ const listMode = resolvePersistedChatSessionListMode(persistedState);
134
154
  return {
135
155
  ...currentState,
136
156
  snapshot: {
137
157
  ...currentState.snapshot,
138
- listMode
158
+ ...(listMode ? { listMode } : {}),
159
+ pinnedSessionKeys: normalizePersistedStringList(snapshot?.pinnedSessionKeys),
160
+ pinnedProjectRoots: normalizePersistedStringList(snapshot?.pinnedProjectRoots),
161
+ collapsedProjectRoots: normalizePersistedStringList(snapshot?.collapsedProjectRoots),
139
162
  }
140
163
  };
141
164
  }
@@ -33,6 +33,9 @@ export type ChatWorkspaceFileTab = {
33
33
  line?: number | null;
34
34
  column?: number | null;
35
35
  rawText?: string | null;
36
+ /** Attachment/binary content URL for workspace-native media preview. */
37
+ contentUrl?: string | null;
38
+ mimeType?: string | null;
36
39
  beforeText?: string | null;
37
40
  afterText?: string | null;
38
41
  patchText?: string | null;
@@ -38,7 +38,7 @@ import { cn } from "@/shared/lib/utils";
38
38
  import { t } from "@/shared/lib/i18n";
39
39
  import { PageLayout, PageHeader } from "@/app/components/layout/page-layout";
40
40
  import {
41
- AlarmClock,
41
+ Search,
42
42
  RefreshCw,
43
43
  Trash2,
44
44
  Play,
@@ -77,16 +77,16 @@ function StatusBadge({ job }: { job: CronJobView }) {
77
77
  return (
78
78
  <span
79
79
  className={cn(
80
- "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold",
80
+ "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium",
81
81
  job.enabled
82
82
  ? "bg-emerald-50 text-emerald-700"
83
- : "bg-gray-100 text-gray-500",
83
+ : "bg-muted text-muted-foreground",
84
84
  )}
85
85
  >
86
86
  <span
87
87
  className={cn(
88
88
  "h-1.5 w-1.5 rounded-full",
89
- job.enabled ? "bg-emerald-400" : "bg-gray-400",
89
+ job.enabled ? "bg-emerald-500" : "bg-muted-foreground/50",
90
90
  )}
91
91
  />
92
92
  {job.enabled ? t("enabled") : t("disabled")}
@@ -104,10 +104,7 @@ function CronJobCard(props: {
104
104
  const [expanded, setExpanded] = useState(false);
105
105
  const [menuOpen, setMenuOpen] = useState(false);
106
106
 
107
- const barColor = job.enabled ? "bg-emerald-400" : "bg-gray-300";
108
-
109
107
  const handleCardClick = (e: React.MouseEvent) => {
110
- // Don't expand when clicking switch or menu
111
108
  const target = e.target as HTMLElement;
112
109
  if (target.closest("[data-no-expand]")) return;
113
110
  setExpanded(!expanded);
@@ -116,44 +113,33 @@ function CronJobCard(props: {
116
113
  return (
117
114
  <div
118
115
  className={cn(
119
- "group relative overflow-hidden rounded-xl border bg-white transition-all duration-150 cursor-pointer",
120
- expanded
121
- ? "border-gray-300 shadow-sm"
122
- : "border-gray-200 hover:border-gray-300 hover:shadow-sm",
116
+ "group overflow-hidden rounded-xl border border-border/70 bg-card transition-colors cursor-pointer",
117
+ expanded ? "bg-muted/20" : "hover:bg-muted/30",
123
118
  )}
124
119
  onClick={handleCardClick}
125
120
  >
126
- {/* Left color bar */}
127
- <div
128
- className={cn(
129
- "absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
130
- barColor,
131
- )}
132
- />
133
-
134
- <div className="pl-4 pr-3 py-3"> {/* Row 1: name + badges + actions */}
135
- <div className="flex items-center gap-3">
121
+ <div className="px-3 py-2.5">
122
+ <div className="flex items-center gap-2.5">
136
123
  <div className="min-w-0 flex-1">
137
124
  <div className="flex items-center gap-2">
138
125
  {expanded ? (
139
- <ChevronDown className="h-3.5 w-3.5 text-gray-400 shrink-0" />
126
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
140
127
  ) : (
141
- <ChevronRight className="h-3.5 w-3.5 text-gray-300 shrink-0 group-hover:text-gray-500 transition-colors" />
128
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/50 shrink-0 group-hover:text-muted-foreground transition-colors" />
142
129
  )}
143
- <span className="text-sm font-semibold text-gray-900 truncate">
130
+ <span className="text-sm font-medium text-foreground truncate">
144
131
  {job.name || job.id}
145
132
  </span>
146
133
  <StatusBadge job={job} />
147
- {job.deleteAfterRun && (
148
- <span className="rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-600">
134
+ {job.deleteAfterRun ? (
135
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
149
136
  {t("cronOneShot")}
150
137
  </span>
151
- )}
138
+ ) : null}
152
139
  </div>
153
140
  </div>
154
141
 
155
- {/* Actions - stop propagation so they don't toggle expand */}
156
- <div className="flex items-center gap-1.5 shrink-0" data-no-expand>
142
+ <div className="flex items-center gap-1 shrink-0" data-no-expand>
157
143
  <Switch
158
144
  checked={job.enabled}
159
145
  onCheckedChange={(checked) => onToggle(job, checked)}
@@ -163,7 +149,7 @@ function CronJobCard(props: {
163
149
  <Button
164
150
  variant="ghost"
165
151
  size="icon"
166
- className="h-8 w-8 text-gray-400 hover:text-gray-600"
152
+ className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-muted"
167
153
  aria-label={t("cronMoreActions")}
168
154
  >
169
155
  <MoreVertical className="h-4 w-4" />
@@ -176,9 +162,9 @@ function CronJobCard(props: {
176
162
  setMenuOpen(false);
177
163
  onRun(job);
178
164
  }}
179
- className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100"
165
+ className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted"
180
166
  >
181
- <Play className="h-3.5 w-3.5 text-gray-500" />
167
+ <Play className="h-3.5 w-3.5 text-muted-foreground" />
182
168
  {t("cronRunNow")}
183
169
  </button>
184
170
  <button
@@ -197,28 +183,27 @@ function CronJobCard(props: {
197
183
  </div>
198
184
  </div>
199
185
 
200
- {/* Row 2: schedule + next run + last run (always visible) */}
201
- <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 pl-6">
186
+ <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground pl-6">
202
187
  <span className="flex items-center gap-1">
203
- <CalendarClock className="h-3 w-3 text-gray-400" />
204
- <span className="font-mono text-gray-600">
188
+ <CalendarClock className="h-3 w-3 text-muted-foreground/70" />
189
+ <span className="font-mono text-foreground/80">
205
190
  {describeCronSchedule(job)}
206
191
  </span>
207
192
  </span>
208
- {job.state.nextRunAt && (
193
+ {job.state.nextRunAt ? (
209
194
  <span className="flex items-center gap-1">
210
- <Clock className="h-3 w-3 text-gray-400" />
195
+ <Clock className="h-3 w-3 text-muted-foreground/70" />
211
196
  <span>{formatRelativeTime(job.state.nextRunAt)}</span>
212
197
  </span>
213
- )}
214
- {job.state.lastRunAt && (
198
+ ) : null}
199
+ {job.state.lastRunAt ? (
215
200
  <span className="flex items-center gap-1" data-no-expand>
216
- <span className="text-gray-300">•</span>
201
+ <span className="text-border">•</span>
217
202
  {job.state.lastStatus === "error" && job.state.lastError ? (
218
203
  <TooltipProvider delayDuration={200}>
219
204
  <Tooltip>
220
205
  <TooltipTrigger asChild>
221
- <span className="inline-flex items-center gap-0.5 text-orange-400 cursor-help">
206
+ <span className="inline-flex items-center gap-0.5 text-orange-500 cursor-help">
222
207
  <AlertCircle className="h-3 w-3" />
223
208
  </span>
224
209
  </TooltipTrigger>
@@ -235,8 +220,8 @@ function CronJobCard(props: {
235
220
  <span
236
221
  className={cn(
237
222
  job.state.lastStatus === "ok"
238
- ? "text-emerald-400"
239
- : "text-gray-400",
223
+ ? "text-emerald-500"
224
+ : "text-muted-foreground/70",
240
225
  )}
241
226
  >
242
227
  {job.state.lastStatus === "ok" ? "✓" : ""}
@@ -244,14 +229,13 @@ function CronJobCard(props: {
244
229
  )}
245
230
  <span>{formatRelativeTime(job.state.lastRunAt)}</span>
246
231
  </span>
247
- )}
232
+ ) : null}
248
233
  </div>
249
234
 
250
- {/* Row 3: message preview (always visible, 2 lines) */}
251
235
  <div className="mt-2 pl-6">
252
236
  <div
253
237
  className={cn(
254
- "text-sm text-gray-600 break-words",
238
+ "text-sm text-muted-foreground break-words",
255
239
  !expanded && "line-clamp-2",
256
240
  )}
257
241
  >
@@ -259,40 +243,38 @@ function CronJobCard(props: {
259
243
  </div>
260
244
  </div>
261
245
 
262
- {/* Expanded details */}
263
- {expanded && (
264
- <div className="mt-3 ml-6 space-y-3 animate-in fade-in-0 slide-in-from-top-1 duration-150">
265
- {/* Metadata grid */}
246
+ {expanded ? (
247
+ <div className="mt-3 ml-6 space-y-3 border-t border-border/60 pt-3">
266
248
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2 text-xs">
267
249
  <div>
268
- <div className="text-[11px] text-gray-400 mb-0.5">
250
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
269
251
  {t("cronSessionLabel")}
270
252
  </div>
271
253
  <div
272
- className="text-gray-700 font-mono text-[11px] truncate"
254
+ className="text-foreground/80 font-mono text-[11px] truncate"
273
255
  title={describeCronSession(job)}
274
256
  >
275
257
  {describeCronSession(job)}
276
258
  </div>
277
259
  </div>
278
260
  <div>
279
- <div className="text-[11px] text-gray-400 mb-0.5">
261
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
280
262
  {t("cronNextRun")}
281
263
  </div>
282
- <div className="text-gray-700">
264
+ <div className="text-foreground/80">
283
265
  {formatCronDate(job.state.nextRunAt)}
284
266
  </div>
285
267
  </div>
286
268
  <div>
287
- <div className="text-[11px] text-gray-400 mb-0.5">
269
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
288
270
  {t("cronLastRun")}
289
271
  </div>
290
- <div className="text-gray-700">
272
+ <div className="text-foreground/80">
291
273
  {formatCronDate(job.state.lastRunAt)}
292
274
  </div>
293
275
  </div>
294
276
  <div>
295
- <div className="text-[11px] text-gray-400 mb-0.5">
277
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
296
278
  {t("cronLastStatus")}
297
279
  </div>
298
280
  <div
@@ -304,26 +286,26 @@ function CronJobCard(props: {
304
286
  ? "text-red-600"
305
287
  : job.state.lastStatus === "skipped"
306
288
  ? "text-amber-600"
307
- : "text-gray-500",
289
+ : "text-muted-foreground",
308
290
  )}
309
291
  >
310
292
  {job.state.lastStatus ?? "-"}
311
293
  </div>
312
294
  </div>
313
295
  <div>
314
- <div className="text-[11px] text-gray-400 mb-0.5">
296
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
315
297
  {t("cronCreatedAt")}
316
298
  </div>
317
- <div className="text-gray-700">
299
+ <div className="text-foreground/80">
318
300
  {formatCronDate(job.createdAt)}
319
301
  </div>
320
302
  </div>
321
303
  <div>
322
- <div className="text-[11px] text-gray-400 mb-0.5">
304
+ <div className="text-[11px] text-muted-foreground/70 mb-0.5">
323
305
  {t("cronId")}
324
306
  </div>
325
307
  <div
326
- className="text-gray-700 font-mono text-[11px] truncate"
308
+ className="text-foreground/80 font-mono text-[11px] truncate"
327
309
  title={job.id}
328
310
  >
329
311
  {job.id}
@@ -331,14 +313,13 @@ function CronJobCard(props: {
331
313
  </div>
332
314
  </div>
333
315
 
334
- {/* Error */}
335
- {job.state.lastError && (
336
- <div className="rounded-lg bg-red-50 border border-red-100 px-3 py-2 text-xs text-red-600 break-words">
316
+ {job.state.lastError ? (
317
+ <div className="rounded-lg bg-red-50/80 border border-red-100 px-3 py-2 text-xs text-red-600 break-words">
337
318
  {job.state.lastError}
338
319
  </div>
339
- )}
320
+ ) : null}
340
321
  </div>
341
- )}
322
+ ) : null}
342
323
  </div>
343
324
  </div>
344
325
  );
@@ -397,7 +378,7 @@ export function CronConfig() {
397
378
  <Button
398
379
  variant="ghost"
399
380
  size="icon"
400
- className="h-9 w-9 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"
381
+ className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted"
401
382
  onClick={() => cronQuery.refetch()}
402
383
  >
403
384
  <RefreshCw
@@ -407,16 +388,16 @@ export function CronConfig() {
407
388
  }
408
389
  />
409
390
 
410
- <div className="mb-5">
411
- <div className="flex flex-wrap gap-3 items-center">
391
+ <div className="mb-4">
392
+ <div className="flex flex-wrap gap-2.5 items-center">
412
393
  <div className="relative flex-1 min-w-[240px]">
394
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/70" />
413
395
  <Input
414
396
  value={query}
415
397
  onChange={(e) => setQuery(e.target.value)}
416
398
  placeholder={t("cronSearchPlaceholder")}
417
399
  className="pl-9"
418
400
  />
419
- <AlarmClock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
420
401
  </div>
421
402
  <div className="min-w-[140px]">
422
403
  <Select
@@ -437,7 +418,7 @@ export function CronConfig() {
437
418
  </SelectContent>
438
419
  </Select>
439
420
  </div>
440
- <div className="text-xs text-gray-400 ml-auto tabular-nums">
421
+ <div className="text-xs text-muted-foreground ml-auto tabular-nums">
441
422
  {jobs.length} / {cronQuery.data?.total ?? 0}
442
423
  </div>
443
424
  </div>
@@ -445,22 +426,24 @@ export function CronConfig() {
445
426
 
446
427
  <div className="flex-1 overflow-auto custom-scrollbar">
447
428
  {cronQuery.isLoading ? (
448
- <div className="flex flex-col items-center justify-center py-16 text-gray-400">
449
- <RefreshCw className="h-6 w-6 animate-spin mb-3" />
429
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
430
+ <RefreshCw className="h-5 w-5 animate-spin mb-3" />
450
431
  <span className="text-sm">{t("cronLoading")}</span>
451
432
  </div>
452
433
  ) : jobs.length === 0 ? (
453
- <div className="flex flex-col items-center justify-center py-16 text-gray-400">
454
- <ListTodo className="h-10 w-10 mb-3 text-gray-300" />
455
- <span className="text-sm font-medium text-gray-500">
434
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
435
+ <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl bg-muted">
436
+ <ListTodo className="h-5 w-5 text-muted-foreground/70" />
437
+ </div>
438
+ <span className="text-sm font-medium text-foreground">
456
439
  {t("cronEmpty")}
457
440
  </span>
458
- <span className="text-xs text-gray-400 mt-1">
441
+ <span className="text-xs text-muted-foreground mt-1">
459
442
  {t("cronEmptyGuide")}
460
443
  </span>
461
444
  </div>
462
445
  ) : (
463
- <div className="space-y-2.5">
446
+ <div className="space-y-2">
464
447
  {jobs.map((job) => (
465
448
  <CronJobCard
466
449
  key={job.id}
@@ -1,11 +1,32 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type * as MarketplaceApi from "@/shared/lib/api";
2
4
  import {
3
5
  createMarketplaceDetailDocId,
4
6
  createMarketplaceDetailDocUrl,
5
7
  MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS,
6
8
  MARKETPLACE_DETAIL_TAB_KIND,
7
9
  } from "@/features/marketplace/components/marketplace-detail-doc";
8
- import { setMarketplaceDetailDocEntry } from "@/features/marketplace/stores/marketplace-detail-doc.store";
10
+ import {
11
+ setMarketplaceDetailDocEntry,
12
+ useMarketplaceDetailDocStore,
13
+ } from "@/features/marketplace/stores/marketplace-detail-doc.store";
14
+
15
+ const mocks = vi.hoisted(() => ({
16
+ fetchMarketplaceSkillContent: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("@/shared/lib/api", async (importOriginal) => {
20
+ const actual = await importOriginal<typeof MarketplaceApi>();
21
+ return {
22
+ ...actual,
23
+ fetchMarketplaceSkillContent: mocks.fetchMarketplaceSkillContent,
24
+ };
25
+ });
26
+
27
+ vi.mock("@/shared/lib/i18n", () => ({
28
+ t: (key: string) => key,
29
+ }));
9
30
 
10
31
  function renderDetail(detailId: string) {
11
32
  const renderer = MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS[MARKETPLACE_DETAIL_TAB_KIND];
@@ -31,6 +52,11 @@ function renderDetail(detailId: string) {
31
52
  }
32
53
 
33
54
  describe("Marketplace detail doc renderer", () => {
55
+ beforeEach(() => {
56
+ mocks.fetchMarketplaceSkillContent.mockReset();
57
+ useMarketplaceDetailDocStore.setState({ entries: {} });
58
+ });
59
+
34
60
  it("renders metadata and markdown content as React content", () => {
35
61
  const detailId = createMarketplaceDetailDocId("skill", "weather");
36
62
  setMarketplaceDetailDocEntry({
@@ -75,7 +101,7 @@ describe("Marketplace detail doc renderer", () => {
75
101
  expect(screen.getAllByText(/<script>alert\(1\)<\/script>/)).toHaveLength(1);
76
102
  });
77
103
 
78
- it("renders loading and missing states inside the React panel", () => {
104
+ it("renders loading state and rehydrates missing skill details after refresh", async () => {
79
105
  const loadingId = createMarketplaceDetailDocId("skill", "loading");
80
106
  setMarketplaceDetailDocEntry({
81
107
  id: loadingId,
@@ -88,7 +114,15 @@ describe("Marketplace detail doc renderer", () => {
88
114
  const { rerender } = renderDetail(loadingId);
89
115
  expect(document.querySelector(".animate-pulse")).toBeTruthy();
90
116
 
91
- const missingId = createMarketplaceDetailDocId("skill", "missing");
117
+ mocks.fetchMarketplaceSkillContent.mockResolvedValue({
118
+ metadataRaw: "name: weather",
119
+ bodyRaw: "# Weather Skill\n\nLocal forecast",
120
+ raw: "# Weather Skill\n\nLocal forecast",
121
+ source: "catalog",
122
+ sourceUrl: "https://example.com/weather",
123
+ });
124
+
125
+ const missingId = createMarketplaceDetailDocId("skill-preview", "weather");
92
126
  const renderer = MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS[MARKETPLACE_DETAIL_TAB_KIND];
93
127
  rerender(
94
128
  <>
@@ -100,7 +134,7 @@ describe("Marketplace detail doc renderer", () => {
100
134
  tab: {
101
135
  id: "tab-1",
102
136
  kind: MARKETPLACE_DETAIL_TAB_KIND,
103
- title: "Missing",
137
+ title: "weather",
104
138
  currentUrl: createMarketplaceDetailDocUrl(missingId),
105
139
  history: [createMarketplaceDetailDocUrl(missingId)],
106
140
  historyIndex: 0,
@@ -110,6 +144,10 @@ describe("Marketplace detail doc renderer", () => {
110
144
  </>,
111
145
  );
112
146
 
113
- expect(screen.getByText("Detail unavailable")).toBeTruthy();
147
+ await waitFor(() => {
148
+ expect(screen.getByRole("heading", { name: "weather", level: 1 })).toBeTruthy();
149
+ });
150
+ expect(mocks.fetchMarketplaceSkillContent).toHaveBeenCalledWith("weather");
151
+ expect(screen.getByText("Local forecast")).toBeTruthy();
114
152
  });
115
153
  });
@@ -8,9 +8,9 @@ import type {
8
8
  import { Skeleton } from '@/shared/components/ui/skeleton';
9
9
  import { t } from '@/shared/lib/i18n';
10
10
  import { cn } from '@/shared/lib/utils';
11
+ import { useMarketplaceDetailDocEntry } from '@/features/marketplace/hooks/use-marketplace-detail-doc-entry';
11
12
  import {
12
13
  setMarketplaceDetailDocEntry,
13
- useMarketplaceDetailDocStore,
14
14
  type MarketplaceDetailDocEntry,
15
15
  } from '@/features/marketplace/stores/marketplace-detail-doc.store';
16
16
 
@@ -422,13 +422,17 @@ function MarketplaceDetailDocContent({ entry }: { entry: MarketplaceDetailDocEnt
422
422
 
423
423
  function MarketplaceDetailDoc({ currentUrl }: { currentUrl: string }) {
424
424
  const detailId = readMarketplaceDetailDocId(currentUrl);
425
- const entry = useMarketplaceDetailDocStore((state) => (detailId ? state.entries[detailId] : undefined));
426
- if (!detailId || !entry) {
425
+ const entry = useMarketplaceDetailDocEntry(detailId);
426
+
427
+ if (!detailId) {
427
428
  return <MarketplaceDetailUnavailable />;
428
429
  }
429
- if (entry.status === 'loading') {
430
+ if (!entry || entry.status === 'loading') {
430
431
  return <MarketplaceDetailSkeleton />;
431
432
  }
433
+ if (entry.status === 'error' && !entry.contentRaw) {
434
+ return <MarketplaceDetailUnavailable />;
435
+ }
432
436
  return <MarketplaceDetailDocContent entry={entry} />;
433
437
  }
434
438
 
@@ -37,7 +37,7 @@ export function FilterPanel({
37
37
  value={searchText}
38
38
  onChange={(event) => onSearchTextChange(event.target.value)}
39
39
  placeholder={searchPlaceholder}
40
- className="h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-9 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40"
40
+ className="h-9 w-full rounded-xl border border-border/75 bg-card pl-9 pr-9 text-sm transition-colors focus:outline-none focus:ring-0 focus:border-border"
41
41
  />
42
42
  {isRefreshing && (
43
43
  <Loader2
@@ -266,21 +266,14 @@ export function McpMarketplacePage() {
266
266
  return (
267
267
  <PageLayout className="flex h-full min-h-0 flex-col pb-0 px-0">
268
268
  <div className="flex flex-col gap-6 w-full max-w-[1400px] h-full min-h-0 mx-auto">
269
-
270
- {/* Modern App Store Hero for MCP */}
271
- <div className="relative overflow-hidden rounded-[32px] bg-gradient-to-br from-[#0F172A] via-[#1E1B4B] to-[#312E81] px-10 py-14 text-white shadow-xl isolate">
272
- <div className="absolute top-0 right-0 -m-32 opacity-30 pointer-events-none mix-blend-screen scale-150 transform-gpu">
273
- <div className="w-[500px] h-[500px] rounded-full bg-gradient-to-tl from-fuchsia-500/40 via-purple-500/30 to-indigo-500/20 blur-[80px]"></div>
274
- </div>
275
- <div className="absolute bottom-0 left-0 -m-32 opacity-20 pointer-events-none mix-blend-screen transform-gpu">
276
- <div className="w-[400px] h-[400px] rounded-full bg-gradient-to-tr from-blue-500/40 to-cyan-500/20 blur-[80px]"></div>
277
- </div>
278
-
269
+ <div className="relative isolate overflow-hidden rounded-[28px] border border-border bg-card px-10 py-12 text-card-foreground shadow-card">
270
+ <div className="absolute inset-y-0 left-0 w-1 bg-primary/80" />
271
+
279
272
  <div className="relative z-10 flex flex-col gap-3">
280
- <h1 className="text-[38px] font-extrabold tracking-[-0.02em] leading-tight drop-shadow-sm text-transparent bg-clip-text bg-gradient-to-r from-white via-white to-white/70">
273
+ <h1 className="text-[38px] font-extrabold leading-tight text-foreground">
281
274
  {t("marketplaceMcpPageTitle")}
282
275
  </h1>
283
- <p className="text-[17px] font-medium text-purple-100/70 max-w-2xl leading-relaxed tracking-wide">
276
+ <p className="max-w-2xl text-[17px] font-medium leading-relaxed text-muted-foreground">
284
277
  {t("marketplaceMcpPageDescription")}
285
278
  </p>
286
279
  </div>