@nextclaw/ui 0.15.2 → 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 (109) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{api-B7Qigvtm.js → api-iNBpppzj.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-CuE0Iiqj.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/{channels-list-page-CTsM6TcP.js → channels-list-page-CCtp0BRS.js} +1 -1
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/{config-split-page-Fbb4PtTf.js → config-split-page-CrOJgB-Y.js} +1 -1
  8. package/dist/assets/{confirm-dialog-D2EG3nqE.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-BGR3HUi1.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-7JTjycxE.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-wXLhk4hl.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-CYAGm5d5.js → doc-browser-context-BHY-0FSD.js} +1 -1
  15. package/dist/assets/{es2015-j1SRCIX5.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-DxuPaDoK.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-BvCp_0lS.js} +1 -1
  18. package/dist/assets/{i18n-provider-B4oMxOFx.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-DeKk9gvE.js → model-config-page-bNNo-XyG.js} +1 -1
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-DsH2LF1q.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/{providers-config-page-BFnGp68F.js → providers-config-page-wLo6CAJw.js} +1 -1
  28. package/dist/assets/{react-UirzUuST.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-eT8BF-8B.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-BelAqiNG.js → runtime-config-page-DDn1sP4G.js} +1 -1
  33. package/dist/assets/{save-Cb2g0BOi.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-BcHjwdsb.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/{search-config-page-zPm6--Ig.js → search-config-page-B-Cf4iiB.js} +1 -1
  36. package/dist/assets/{secrets-config-page-Cxyp-KuQ.js → secrets-config-page-DPKsY8Iw.js} +1 -1
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-CdSEOwVM.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-CTAtGexr.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-DPvakJ0a.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-D-WsKoT1.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CjkOaHW7.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config--hx5oxQn.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CD9a_SK1.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +26 -26
  50. package/package.json +9 -9
  51. package/src/app/components/layout/sidebar-items.tsx +3 -3
  52. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  53. package/src/app/components/layout/sidebar.tsx +1 -1
  54. package/src/app/styles/design-system.css +17 -4
  55. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -1
  56. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  57. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  58. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  59. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  60. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  61. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  62. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  63. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +1 -1
  64. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  65. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  66. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  67. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +67 -0
  68. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +1 -1
  69. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  70. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +1 -1
  71. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +174 -22
  72. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  73. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  74. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  75. package/src/features/cron/components/cron-config.tsx +63 -80
  76. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  77. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  78. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  79. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  80. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  81. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  82. package/src/features/side-dock/components/side-dock.tsx +3 -3
  83. package/src/index.css +239 -13
  84. package/src/shared/components/actions/action-link.tsx +1 -1
  85. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  86. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  87. package/src/shared/components/ui/button.tsx +1 -1
  88. package/src/shared/components/ui/dialog.tsx +1 -1
  89. package/src/shared/components/ui/input.tsx +1 -1
  90. package/src/shared/components/ui/select.tsx +1 -1
  91. package/src/shared/components/ui/switch.tsx +1 -1
  92. package/src/shared/components/ui/tabs.tsx +1 -1
  93. package/src/shared/components/ui/textarea.tsx +1 -1
  94. package/src/shared/lib/i18n/locales/en-US/chat.json +83 -0
  95. package/src/shared/lib/i18n/locales/zh-CN/chat.json +83 -0
  96. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  97. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  98. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  99. package/dist/assets/index-CvrvblgX.css +0 -1
  100. package/dist/assets/index-Obd4JYU2.js +0 -104
  101. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  102. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  103. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  104. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  105. package/dist/assets/remote-CDkHZtA8.js +0 -1
  106. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  107. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  108. package/dist/assets/switch-BU_UEULb.js +0 -1
  109. package/dist/assets/x-DT5x45JU.js +0 -1
@@ -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
@@ -0,0 +1,84 @@
1
+ import { useEffect } from 'react';
2
+ import { fetchMarketplaceSkillContent } from '@/shared/lib/api';
3
+ import { t } from '@/shared/lib/i18n';
4
+ import {
5
+ setMarketplaceDetailDocEntry,
6
+ useMarketplaceDetailDocStore,
7
+ } from '@/features/marketplace/stores/marketplace-detail-doc.store';
8
+
9
+ function parseMarketplaceDetailScopeKey(
10
+ detailId: string,
11
+ ): { scope: string; key: string } | null {
12
+ const separator = detailId.indexOf(':');
13
+ if (separator <= 0 || separator >= detailId.length - 1) {
14
+ return null;
15
+ }
16
+ return {
17
+ scope: detailId.slice(0, separator),
18
+ key: detailId.slice(separator + 1),
19
+ };
20
+ }
21
+
22
+ function isRehydratableSkillScope(scope: string): boolean {
23
+ return scope === 'skill' || scope === 'skill-preview';
24
+ }
25
+
26
+ export function useMarketplaceDetailDocEntry(detailId: string | null) {
27
+ const entry = useMarketplaceDetailDocStore((state) =>
28
+ detailId ? state.entries[detailId] : undefined,
29
+ );
30
+
31
+ useEffect(() => {
32
+ if (!detailId || useMarketplaceDetailDocStore.getState().entries[detailId]) {
33
+ return;
34
+ }
35
+ const parsed = parseMarketplaceDetailScopeKey(detailId);
36
+ if (!parsed || !isRehydratableSkillScope(parsed.scope)) {
37
+ return;
38
+ }
39
+
40
+ let cancelled = false;
41
+ const title = parsed.key;
42
+ const typeLabel = t('marketplaceTypeSkill');
43
+
44
+ void fetchMarketplaceSkillContent(parsed.key)
45
+ .then((content) => {
46
+ if (cancelled) {
47
+ return;
48
+ }
49
+ setMarketplaceDetailDocEntry({
50
+ id: detailId,
51
+ title,
52
+ typeLabel,
53
+ spec: parsed.key,
54
+ status: 'ready',
55
+ summary: t('marketplaceInstalledLocalSummary'),
56
+ metadataRaw: content.metadataRaw,
57
+ contentRaw: content.bodyRaw || content.raw,
58
+ sourceUrl: content.sourceUrl,
59
+ sourceLabel: content.source ? `Source (${content.source})` : undefined,
60
+ });
61
+ })
62
+ .catch(() => {
63
+ if (cancelled) {
64
+ return;
65
+ }
66
+ setMarketplaceDetailDocEntry({
67
+ id: detailId,
68
+ title,
69
+ typeLabel,
70
+ spec: parsed.key,
71
+ status: 'error',
72
+ summary: t('marketplaceInstalledLocalSummary'),
73
+ metadataRaw: JSON.stringify({ skill: parsed.key }, null, 2),
74
+ contentRaw: t('marketplaceDetailUnavailableDescription'),
75
+ });
76
+ });
77
+
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [detailId]);
82
+
83
+ return entry;
84
+ }
@@ -204,7 +204,7 @@ function PanelAppsEmptyGuide({
204
204
  <button
205
205
  type="button"
206
206
  onClick={onRefresh}
207
- className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
207
+ className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
208
208
  >
209
209
  <RefreshCw className="h-3.5 w-3.5" />
210
210
  {t('panelAppsRefresh')}
@@ -245,7 +245,7 @@ function PanelAppsEmptyGuideStep({
245
245
  <button
246
246
  type="button"
247
247
  onClick={onAction}
248
- className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
248
+ className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
249
249
  >
250
250
  {content}
251
251
  </button>
@@ -80,7 +80,7 @@ export function RightPanelResourceHomePage({ open }: RightPanelResourceHomePageP
80
80
  key={item.id}
81
81
  type="button"
82
82
  onClick={() => openTarget(item.target)}
83
- className="group flex min-h-[68px] flex-col items-center gap-1.5 rounded-md px-1.5 py-1.5 text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
83
+ className="group flex min-h-[68px] flex-col items-center gap-1.5 rounded-md px-1.5 py-1.5 text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
84
84
  >
85
85
  <span className={cn('flex h-10 w-10 shrink-0 items-center justify-center rounded-md transition-colors', view.accentClassName, view.hoverAccentClassName)}>
86
86
  {view.icon}
@@ -95,7 +95,7 @@ function SideDockButton({
95
95
  onClick={() => onOpen(item)}
96
96
  className={cn(
97
97
  'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors',
98
- 'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
98
+ 'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
99
99
  active
100
100
  ? 'bg-primary/10 text-primary'
101
101
  : 'bg-transparent',
@@ -113,7 +113,7 @@ function SideDockButton({
113
113
  event.stopPropagation();
114
114
  onUnpin(item);
115
115
  }}
116
- className="absolute -right-1 -top-1 hidden h-4 w-4 items-center justify-center rounded-full bg-background text-muted-foreground shadow-sm ring-1 ring-border/70 transition-colors hover:bg-muted hover:text-foreground group-hover:flex focus-visible:flex focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
116
+ className="absolute -right-1 -top-1 hidden h-4 w-4 items-center justify-center rounded-full bg-background text-muted-foreground shadow-sm ring-1 ring-border/70 transition-colors hover:bg-muted hover:text-foreground group-hover:flex focus-visible:flex focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
117
117
  >
118
118
  <X className="h-3 w-3" aria-hidden="true" />
119
119
  </button>
@@ -144,7 +144,7 @@ function SideDockActionButton({
144
144
  onClick={onClick}
145
145
  className={cn(
146
146
  'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors',
147
- 'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
147
+ 'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
148
148
  )}
149
149
  >
150
150
  <Icon className="h-5 w-5" aria-hidden="true" />