@nextclaw/ui 0.15.5 → 0.15.7

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 (169) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/api-V8m62-13.js +13 -0
  3. package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
  10. package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
  12. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
  13. package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
  20. package/dist/assets/index-DtWf7kk9.css +1 -0
  21. package/dist/assets/index-UnY4HtVa.js +2 -0
  22. package/dist/assets/loader-circle-BRSfaztA.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
  27. package/dist/assets/plus-BgMBV9HJ.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
  30. package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
  33. package/dist/assets/remote-C1y3ARHu.js +1 -0
  34. package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
  35. package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
  40. package/dist/assets/security-config-D8me3pXl.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
  48. package/dist/assets/use-config-CY4uF2Ou.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
  51. package/dist/assets/x-C6cM8wkT.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +8 -8
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
  56. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  58. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  62. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  65. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  67. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  68. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
  69. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
  71. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  72. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
  73. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
  74. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  75. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  76. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
  77. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  78. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  79. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
  80. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  81. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  82. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  83. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  84. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  85. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  86. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
  87. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  88. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  89. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  93. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
  96. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  97. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  98. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  99. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  100. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  101. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  102. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  104. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  106. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  108. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  109. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  110. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  112. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  113. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  114. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  115. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  116. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  117. package/src/features/settings/components/config/provider-form.tsx +0 -4
  118. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  119. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  120. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  121. package/src/features/system-status/index.ts +9 -0
  122. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  123. package/src/index.css +10 -16
  124. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  125. package/src/shared/components/common/brand-header.tsx +121 -35
  126. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  127. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  128. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  129. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  130. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  131. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  132. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  133. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  134. package/src/shared/hooks/use-projects.ts +28 -0
  135. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  136. package/src/shared/hooks/use-server-path-search.ts +40 -0
  137. package/src/shared/lib/api/index.ts +1 -0
  138. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  139. package/src/shared/lib/api/types.ts +27 -0
  140. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  143. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  144. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  145. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
  147. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  148. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  149. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  150. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  151. package/dist/assets/api-IlX_CbwU.js +0 -13
  152. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  153. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  154. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  155. package/dist/assets/index-0av6WO9-.css +0 -1
  156. package/dist/assets/index-CT0iYlxV.js +0 -2
  157. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  158. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  159. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  160. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  161. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  162. package/dist/assets/remote-BaVECEZw.js +0 -1
  163. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  164. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  165. package/dist/assets/security-config-BPzet76z.js +0 -1
  166. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  167. package/dist/assets/x-B3o1dWR5.js +0 -1
  168. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  169. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef } from "react";
1
2
  import { WorkspaceDocxPreview } from "./file-content-preview/workspace-docx-preview";
2
3
  import { WorkspacePresentationPreview } from "./file-content-preview/workspace-presentation-preview";
3
4
  import { WorkspaceSpreadsheetPreview } from "./file-content-preview/workspace-spreadsheet-preview";
@@ -115,14 +116,97 @@ function WorkspaceUnsupportedContent({
115
116
  );
116
117
  }
117
118
 
119
+ function readHtmlDocumentHeight(document: Document): number {
120
+ const { body, documentElement } = document;
121
+ return Math.ceil(
122
+ Math.max(
123
+ body?.clientHeight ?? 0,
124
+ body?.offsetHeight ?? 0,
125
+ body?.scrollHeight ?? 0,
126
+ documentElement.clientHeight,
127
+ documentElement.offsetHeight,
128
+ documentElement.scrollHeight,
129
+ ),
130
+ );
131
+ }
132
+
133
+ function WorkspaceHtmlPreview({
134
+ contentUrl,
135
+ label,
136
+ onContentHeightChange,
137
+ }: {
138
+ contentUrl: string;
139
+ label: string;
140
+ onContentHeightChange?: (height: number) => void;
141
+ }) {
142
+ const iframeRef = useRef<HTMLIFrameElement>(null);
143
+ const observerCleanupRef = useRef<(() => void) | null>(null);
144
+
145
+ useEffect(
146
+ () => () => {
147
+ observerCleanupRef.current?.();
148
+ },
149
+ [],
150
+ );
151
+
152
+ const observeContentHeight = () => {
153
+ observerCleanupRef.current?.();
154
+ observerCleanupRef.current = null;
155
+ const iframe = iframeRef.current;
156
+ if (
157
+ !iframe ||
158
+ !onContentHeightChange ||
159
+ typeof ResizeObserver === "undefined"
160
+ ) {
161
+ return;
162
+ }
163
+
164
+ try {
165
+ const document = iframe.contentDocument;
166
+ if (!document) {
167
+ return;
168
+ }
169
+ const reportHeight = () => {
170
+ const height = readHtmlDocumentHeight(document);
171
+ if (height > 0) {
172
+ onContentHeightChange(height);
173
+ }
174
+ };
175
+ const observer = new ResizeObserver(reportHeight);
176
+ observer.observe(document.documentElement);
177
+ if (document.body) {
178
+ observer.observe(document.body);
179
+ }
180
+ reportHeight();
181
+ observerCleanupRef.current = () => observer.disconnect();
182
+ } catch {
183
+ // Cross-origin documents keep the caller's bounded fallback height.
184
+ }
185
+ };
186
+
187
+ return (
188
+ <iframe
189
+ ref={iframeRef}
190
+ allowFullScreen
191
+ className="h-full w-full border-0 bg-white"
192
+ data-testid="workspace-html-preview"
193
+ src={contentUrl}
194
+ title={label}
195
+ onLoad={observeContentHeight}
196
+ />
197
+ );
198
+ }
199
+
118
200
  export function WorkspaceFileContentPreview({
119
201
  contentUrl,
120
202
  kind,
121
203
  label,
204
+ onHtmlContentHeightChange,
122
205
  }: {
123
206
  contentUrl: string;
124
207
  kind: WorkspaceFileContentKind;
125
208
  label: string;
209
+ onHtmlContentHeightChange?: (height: number) => void;
126
210
  }) {
127
211
  if (kind === "image") {
128
212
  return (
@@ -165,14 +249,21 @@ export function WorkspaceFileContentPreview({
165
249
  </div>
166
250
  );
167
251
  }
168
- if (kind === "pdf" || kind === "html") {
252
+ if (kind === "html") {
253
+ return (
254
+ <WorkspaceHtmlPreview
255
+ contentUrl={contentUrl}
256
+ label={label}
257
+ onContentHeightChange={onHtmlContentHeightChange}
258
+ />
259
+ );
260
+ }
261
+ if (kind === "pdf") {
169
262
  return (
170
263
  <iframe
171
264
  allowFullScreen
172
265
  className="h-full w-full border-0 bg-white"
173
- data-testid={
174
- kind === "pdf" ? "workspace-content-pdf" : "workspace-html-preview"
175
- }
266
+ data-testid="workspace-content-pdf"
176
267
  src={contentUrl}
177
268
  title={label}
178
269
  />
@@ -215,6 +215,29 @@ describe('buildChatRunMetadata', () => {
215
215
  ).not.toHaveProperty('ui_inline_tokens');
216
216
  });
217
217
 
218
+ it('adds structured workspace references to run metadata', () => {
219
+ expect(
220
+ buildChatRunMetadata({
221
+ composerNodes: [
222
+ createChatComposerTokenNode({
223
+ tokenKind: 'workspace_file',
224
+ tokenKey: 'src/file name.ts',
225
+ label: 'file name.ts',
226
+ }),
227
+ ],
228
+ }),
229
+ ).toMatchObject({
230
+ ui_inline_tokens: [
231
+ {
232
+ kind: 'workspace_file',
233
+ key: 'src/file name.ts',
234
+ label: 'file name.ts',
235
+ rawText: '@file:src%2Ffile%20name.ts',
236
+ },
237
+ ],
238
+ });
239
+ });
240
+
218
241
  it('serializes child session materialization metadata for side chat drafts', () => {
219
242
  expect(
220
243
  buildChatRunMetadata({
@@ -62,6 +62,7 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
62
62
 
63
63
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
64
64
  useMarketplaceItems: () => mocks.itemsQuery,
65
+ useMarketplaceRecentItems: () => mocks.itemsQuery,
65
66
  useMarketplaceInstalled: () => mocks.installedQuery,
66
67
  useMarketplaceSkillScenes: () => ({
67
68
  data: { scenes: [] },
@@ -31,6 +31,7 @@ const mocks = vi.hoisted(() => ({
31
31
  openExternalUrl: vi.fn(),
32
32
  routeParams: {} as { scene?: string },
33
33
  itemsQuery: null as unknown as ItemsQueryState,
34
+ recentItemsQuery: null as unknown as ItemsQueryState,
34
35
  installedQuery: null as unknown as InstalledQueryState,
35
36
  installMutation: {
36
37
  mutateAsync: vi.fn(),
@@ -82,6 +83,7 @@ vi.mock("@/shared/lib/host-capabilities", () => ({
82
83
 
83
84
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
84
85
  useMarketplaceItems: () => mocks.itemsQuery,
86
+ useMarketplaceRecentItems: () => mocks.recentItemsQuery,
85
87
  useMarketplaceSkillScenes: () => ({
86
88
  data: {
87
89
  scenes: [
@@ -135,6 +137,7 @@ function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
135
137
  isError: false,
136
138
  error: null,
137
139
  hasNextPage: false,
140
+ fetchNextPage: vi.fn(),
138
141
  ...overrides,
139
142
  };
140
143
  }
@@ -172,6 +175,7 @@ describe("MarketplacePage", () => {
172
175
  mocks.manageMutation.isPending = false;
173
176
  mocks.manageMutation.variables = undefined;
174
177
  mocks.itemsQuery = createItemsQuery();
178
+ mocks.recentItemsQuery = createItemsQuery();
175
179
  mocks.installedQuery = createInstalledQuery();
176
180
  });
177
181
 
@@ -216,6 +220,64 @@ describe("MarketplacePage", () => {
216
220
  expect(screen.getByText("Web Search")).toBeTruthy();
217
221
  });
218
222
 
223
+ it("shows the stable catalog total instead of a loaded-page fraction", () => {
224
+ mocks.itemsQuery = createItemsQuery({
225
+ data: {
226
+ total: 36,
227
+ page: 1,
228
+ pageSize: 20,
229
+ totalPages: 2,
230
+ sort: "relevance",
231
+ items: [createMarketplaceItem()],
232
+ } satisfies MarketplaceListView,
233
+ hasNextPage: true,
234
+ });
235
+
236
+ render(<MarketplacePage forcedType="skills" />);
237
+
238
+ expect(screen.getByText("36 skills")).toBeTruthy();
239
+ expect(screen.queryByText("1 / 36")).toBeNull();
240
+ });
241
+
242
+ it("renders recently updated skills from the dedicated updated query", () => {
243
+ mocks.itemsQuery = createItemsQuery({
244
+ data: {
245
+ total: 4,
246
+ page: 1,
247
+ pageSize: 20,
248
+ totalPages: 1,
249
+ sort: "relevance",
250
+ items: [
251
+ createMarketplaceItem({ name: "Relevant One" }),
252
+ createMarketplaceItem({ id: "2", slug: "2", name: "Relevant Two" }),
253
+ createMarketplaceItem({ id: "3", slug: "3", name: "Relevant Three" }),
254
+ createMarketplaceItem({ id: "4", slug: "4", name: "Relevant Four" }),
255
+ ],
256
+ } satisfies MarketplaceListView,
257
+ });
258
+ mocks.recentItemsQuery = createItemsQuery({
259
+ data: {
260
+ total: 1,
261
+ page: 1,
262
+ pageSize: 6,
263
+ totalPages: 1,
264
+ sort: "updated",
265
+ items: [
266
+ createMarketplaceItem({
267
+ id: "recent",
268
+ slug: "recent",
269
+ name: "Actually Recent",
270
+ updatedAt: "2026-07-17T00:00:00.000Z",
271
+ }),
272
+ ],
273
+ } satisfies MarketplaceListView,
274
+ });
275
+
276
+ render(<MarketplacePage forcedType="skills" />);
277
+
278
+ expect(screen.getByText("Actually Recent")).toBeTruthy();
279
+ });
280
+
219
281
  it("does not render a redundant type label in skill cards", () => {
220
282
  mocks.itemsQuery = createItemsQuery({
221
283
  data: {
@@ -72,6 +72,7 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
72
72
 
73
73
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
74
74
  useMarketplaceItems: () => mocks.itemsQuery,
75
+ useMarketplaceRecentItems: () => mocks.itemsQuery,
75
76
  useMarketplaceSkillScenes: () => mocks.scenesQuery,
76
77
  useMarketplaceSkillSceneCounts: () => new Map([["development-debugging", 2]]),
77
78
  useMarketplaceInstalled: () => ({
@@ -33,7 +33,7 @@ export type MarketplaceShelfEntry = {
33
33
  };
34
34
 
35
35
  export function MarketplaceCuratedShelves(props: {
36
- entries: MarketplaceShelfEntry[];
36
+ recentEntries: MarketplaceShelfEntry[];
37
37
  scenes: MarketplaceSceneView[];
38
38
  isScenesLoading: boolean;
39
39
  isItemsLoading: boolean;
@@ -44,7 +44,7 @@ export function MarketplaceCuratedShelves(props: {
44
44
  onOpenScene: (scene: string) => void;
45
45
  }) {
46
46
  const {
47
- entries,
47
+ recentEntries,
48
48
  scenes,
49
49
  isScenesLoading,
50
50
  isItemsLoading,
@@ -55,10 +55,6 @@ export function MarketplaceCuratedShelves(props: {
55
55
  onOpenScene,
56
56
  } = props;
57
57
  const localeFallbacks = buildLocaleFallbacks(language);
58
- const recentEntries = [...entries]
59
- .sort((left, right) => compareUpdatedAt(left.item, right.item))
60
- .slice(0, 6);
61
-
62
58
  return (
63
59
  <div className="mb-4 space-y-5">
64
60
  <section className="space-y-2.5">
@@ -345,15 +341,6 @@ function ShelfHeader({
345
341
  );
346
342
  }
347
343
 
348
- function compareUpdatedAt(left: MarketplaceItemSummary, right: MarketplaceItemSummary) {
349
- const leftTs = Date.parse(left.updatedAt);
350
- const rightTs = Date.parse(right.updatedAt);
351
- if (Number.isNaN(leftTs) || Number.isNaN(rightTs)) {
352
- return right.updatedAt.localeCompare(left.updatedAt);
353
- }
354
- return rightTs - leftTs;
355
- }
356
-
357
344
  function readLocalized(text: MarketplaceShelfLocalizedText, language: string) {
358
345
  return language.startsWith("zh") ? text.zh : text.en;
359
346
  }
@@ -6,6 +6,7 @@ import { useI18n } from "@/app/components/i18n-provider";
6
6
  import {
7
7
  useMarketplaceInstalled,
8
8
  useMarketplaceItems,
9
+ useMarketplaceRecentItems,
9
10
  } from "@/features/marketplace/hooks/use-marketplace";
10
11
  import {
11
12
  FilterPanel,
@@ -77,6 +78,10 @@ export function MarketplacePage({
77
78
  sort,
78
79
  pageSize: PAGE_SIZE,
79
80
  });
81
+ const recentItemsQuery = useMarketplaceRecentItems(
82
+ typeFilter,
83
+ scope === "all" && !searchText.trim() && !query && !sceneParam,
84
+ );
80
85
 
81
86
  const {
82
87
  containerRef: listContainerRef,
@@ -115,6 +120,7 @@ export function MarketplacePage({
115
120
 
116
121
  const curatedSceneRoute = useMarketplaceCuratedSceneRoute({
117
122
  items: listModel.allItems,
123
+ recentItems: recentItemsQuery.data?.items ?? [],
118
124
  installedRecordLookup: listModel.installedRecordLookup,
119
125
  scene: sceneParam,
120
126
  forcedType,
@@ -201,10 +207,12 @@ export function MarketplacePage({
201
207
 
202
208
  {curatedSceneRoute.showShelves && (
203
209
  <MarketplaceCuratedShelves
204
- entries={curatedSceneRoute.entries}
210
+ recentEntries={curatedSceneRoute.recentEntries}
205
211
  scenes={curatedSceneRoute.scenes}
206
212
  isScenesLoading={curatedSceneRoute.isScenesLoading}
207
- isItemsLoading={listModel.showListSkeleton}
213
+ isItemsLoading={
214
+ listModel.showListSkeleton || recentItemsQuery.isLoading
215
+ }
208
216
  language={language}
209
217
  installState={installState}
210
218
  onOpen={(entry) => void openItemDetail(entry.item, entry.record)}
@@ -1,7 +1,10 @@
1
1
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
2
  import { renderHook, waitFor } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
- import { useMarketplaceItems } from "@/features/marketplace/hooks/use-marketplace";
4
+ import {
5
+ useMarketplaceItems,
6
+ useMarketplaceRecentItems,
7
+ } from "@/features/marketplace/hooks/use-marketplace";
5
8
  import type { MarketplaceListView } from "@/shared/lib/api";
6
9
 
7
10
  const mocks = vi.hoisted(() => ({
@@ -106,4 +109,26 @@ describe("useMarketplaceItems", () => {
106
109
  expect(result.current.data?.items[0]?.name).toBe("Search Skill");
107
110
  });
108
111
  });
112
+
113
+ it("loads the recently updated shelf from its own updated query", async () => {
114
+ mocks.fetchMarketplaceItems.mockResolvedValueOnce(
115
+ createMarketplaceList("Recently Updated Skill"),
116
+ );
117
+
118
+ const { result } = renderHook(() => useMarketplaceRecentItems("skill"), {
119
+ wrapper: createWrapper(),
120
+ });
121
+
122
+ await waitFor(() => {
123
+ expect(result.current.data?.items[0]?.name).toBe(
124
+ "Recently Updated Skill",
125
+ );
126
+ });
127
+ expect(mocks.fetchMarketplaceItems).toHaveBeenCalledWith({
128
+ type: "skill",
129
+ sort: "updated",
130
+ page: 1,
131
+ pageSize: 6,
132
+ });
133
+ });
109
134
  });
@@ -12,6 +12,7 @@ import { useMemo } from "react";
12
12
 
13
13
  type MarketplaceCuratedSceneRouteParams = {
14
14
  items: MarketplaceItemSummary[];
15
+ recentItems: MarketplaceItemSummary[];
15
16
  installedRecordLookup: Map<string, MarketplaceInstalledRecord>;
16
17
  scene?: string;
17
18
  forcedType?: "skills";
@@ -28,6 +29,7 @@ export function useMarketplaceCuratedSceneRoute(
28
29
  ) {
29
30
  const {
30
31
  items,
32
+ recentItems,
31
33
  installedRecordLookup,
32
34
  scene,
33
35
  forcedType,
@@ -62,6 +64,14 @@ export function useMarketplaceCuratedSceneRoute(
62
64
  })),
63
65
  [items, installedRecordLookup],
64
66
  );
67
+ const recentEntries = useMemo(
68
+ () =>
69
+ recentItems.map((item) => ({
70
+ item,
71
+ record: findInstalledRecordForItem(item, installedRecordLookup),
72
+ })),
73
+ [installedRecordLookup, recentItems],
74
+ );
65
75
  const selectedScene = useMemo(() => {
66
76
  const normalizedScene = scene?.trim();
67
77
  if (!normalizedScene) {
@@ -94,6 +104,7 @@ export function useMarketplaceCuratedSceneRoute(
94
104
 
95
105
  return {
96
106
  entries,
107
+ recentEntries,
97
108
  scenes,
98
109
  selectedScene,
99
110
  sceneEntries: entries,
@@ -103,7 +103,7 @@ export function useMarketplaceListModel(params: MarketplaceListModelParams) {
103
103
  ? t("loading")
104
104
  : `${installedEntries.length} ${t("marketplaceInstalledSkillsCountSuffix")}`
105
105
  : catalogView
106
- ? `${allItems.length} / ${catalogView.total}`
106
+ ? `${catalogView.total} ${t("marketplaceSkillsCountSuffix")}`
107
107
  : t("loading");
108
108
  const error =
109
109
  scope === "all" && catalogError
@@ -25,6 +25,8 @@ import type {
25
25
  import { collapseMarketplaceListPages } from '@/features/marketplace/utils/marketplace-list-pages.utils';
26
26
  import { useMemo } from 'react';
27
27
 
28
+ const MARKETPLACE_BACKGROUND_REFRESH_MS = 30_000;
29
+
28
30
  export function useMarketplaceItems(params: MarketplaceListParams) {
29
31
  const query = useInfiniteQuery({
30
32
  queryKey: ['marketplace-items', params],
@@ -45,7 +47,11 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
45
47
  ? previousData
46
48
  : undefined;
47
49
  },
48
- staleTime: 15_000
50
+ staleTime: 15_000,
51
+ refetchInterval: params.q ? false : MARKETPLACE_BACKGROUND_REFRESH_MS,
52
+ refetchOnMount: 'always',
53
+ refetchOnReconnect: 'always',
54
+ refetchOnWindowFocus: 'always'
49
55
  });
50
56
 
51
57
  const data = useMemo(() => collapseMarketplaceListPages(query.data), [query.data]);
@@ -56,6 +62,27 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
56
62
  };
57
63
  }
58
64
 
65
+ export function useMarketplaceRecentItems(
66
+ type: MarketplaceItemType,
67
+ enabled = true
68
+ ) {
69
+ return useQuery({
70
+ queryKey: ['marketplace-recent-items', type],
71
+ queryFn: () => fetchMarketplaceItems({
72
+ type,
73
+ sort: 'updated',
74
+ page: 1,
75
+ pageSize: 6
76
+ }),
77
+ enabled,
78
+ staleTime: 15_000,
79
+ refetchInterval: MARKETPLACE_BACKGROUND_REFRESH_MS,
80
+ refetchOnMount: 'always',
81
+ refetchOnReconnect: 'always',
82
+ refetchOnWindowFocus: 'always'
83
+ });
84
+ }
85
+
59
86
  function hasSameMarketplaceListSurface(
60
87
  previousParams: MarketplaceListParams,
61
88
  nextParams: MarketplaceListParams
@@ -230,10 +230,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
230
230
 
231
231
  const handleAddModel = () => {
232
232
  const result = addProviderLocalModel(models, modelDraft, providerModelAliases);
233
- if (result.errorKey) {
234
- toast.error(t('providerModelInvalidProviderPrefix'));
235
- return;
236
- }
237
233
  setModels(result.models);
238
234
  setModelDraft(result.draft);
239
235
  };
@@ -22,10 +22,9 @@ describe('provider form model defaults', () => {
22
22
  });
23
23
 
24
24
  it('serializes local model ids with the providerId prefix', () => {
25
- expect(serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4'], 'openai-work')).toEqual([
26
- 'openai-work/gpt-5.5',
27
- 'openai-work/gpt-5.4'
28
- ]);
25
+ expect(
26
+ serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4', 'bedrock/claude-fable-5'], 'openai-work')
27
+ ).toEqual(['openai-work/gpt-5.5', 'openai-work/gpt-5.4', 'openai-work/bedrock/claude-fable-5']);
29
28
  });
30
29
 
31
30
  it('builds a minimal save payload from changed provider fields', () => {
@@ -57,15 +56,14 @@ describe('provider form model defaults', () => {
57
56
  });
58
57
  });
59
58
 
60
- it('adds local model drafts without accepting provider prefixes', () => {
59
+ it('adds provider-local model ids with nested namespace segments', () => {
61
60
  expect(addProviderLocalModel([], ' gpt-5.5 ', ['openai-work'])).toEqual({
62
61
  models: ['gpt-5.5'],
63
62
  draft: ''
64
63
  });
65
- expect(addProviderLocalModel([], ' other/gpt-5.5 ', ['openai-work'])).toEqual({
66
- models: [],
67
- draft: ' other/gpt-5.5 ',
68
- errorKey: 'providerModelInvalidProviderPrefix'
64
+ expect(addProviderLocalModel([], ' bedrock/claude-fable-5 ', ['openrouter'])).toEqual({
65
+ models: ['bedrock/claude-fable-5'],
66
+ draft: ''
69
67
  });
70
68
  });
71
69
 
@@ -8,7 +8,6 @@ import {
8
8
  type AddProviderModelResult = {
9
9
  models: string[];
10
10
  draft: string;
11
- errorKey?: 'providerModelInvalidProviderPrefix';
12
11
  };
13
12
 
14
13
  export function addProviderLocalModel(models: string[], draft: string, aliases: string[]): AddProviderModelResult {
@@ -16,9 +15,6 @@ export function addProviderLocalModel(models: string[], draft: string, aliases:
16
15
  if (!next) {
17
16
  return { models, draft };
18
17
  }
19
- if (next.includes('/')) {
20
- return { models, draft, errorKey: 'providerModelInvalidProviderPrefix' };
21
- }
22
18
  if (models.includes(next)) {
23
19
  return { models, draft: '' };
24
20
  }
@@ -0,0 +1,33 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { getLanguage } from '@/shared/lib/i18n';
3
+ import {
4
+ fetchReleaseNotesData,
5
+ resolveReleaseNotesHtmlUrl,
6
+ resolveVersionReleaseNotesDataUrl,
7
+ type ReleaseNotesLink,
8
+ type ReleaseNotesLocale
9
+ } from '@/features/system-status/utils/update-release-notes.utils';
10
+
11
+ function getReleaseNotesLocale(): ReleaseNotesLocale {
12
+ return getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
13
+ }
14
+
15
+ export function useCurrentVersionReleaseNotesLink(productVersion: string | undefined): ReleaseNotesLink | null {
16
+ const dataUrl = resolveVersionReleaseNotesDataUrl(productVersion);
17
+ const locale = getReleaseNotesLocale();
18
+ const releaseNotesQuery = useQuery({
19
+ queryKey: ['current-release-notes', dataUrl],
20
+ queryFn: async () => await fetchReleaseNotesData(dataUrl ?? ''),
21
+ enabled: Boolean(dataUrl),
22
+ retry: false,
23
+ staleTime: 5 * 60 * 1000
24
+ });
25
+ const url = resolveReleaseNotesHtmlUrl(releaseNotesQuery.data, locale);
26
+ const version = productVersion?.trim();
27
+ return url && version
28
+ ? {
29
+ url,
30
+ versionLabel: `v${version}`
31
+ }
32
+ : null;
33
+ }
@@ -2,7 +2,16 @@ export { useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus,
2
2
  export { systemStatusManager } from './managers/system-status.manager';
3
3
  export { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
4
4
  export { runtimeUpdateManager } from './managers/runtime-update.manager';
5
+ export { useCurrentVersionReleaseNotesLink } from './hooks/use-current-version-release-notes-link';
6
+ export {
7
+ fetchReleaseNotesData,
8
+ resolveReleaseNotesHtmlUrl,
9
+ resolveUpdateReleaseNotesLink,
10
+ resolveVersionReleaseNotesDataUrl,
11
+ } from './utils/update-release-notes.utils';
5
12
  export type { SystemStatusState, SystemStatusView } from './types/system-status.types';
6
13
  export { useSystemStatusStore } from './stores/system-status.store';
7
14
  export { useRuntimeUpdateStore } from './stores/runtime-update.store';
15
+ export type { RuntimeUpdateBusyAction } from './stores/runtime-update.store';
16
+ export type { ReleaseNotesLink, ReleaseNotesLocale } from './utils/update-release-notes.utils';
8
17
  export { SecurityConfig } from './components/security-config';