@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.
- package/CHANGELOG.md +41 -0
- package/dist/assets/api-V8m62-13.js +13 -0
- package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/use-config-CY4uF2Ou.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/index-CT0iYlxV.js +0 -2
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- 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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
recentEntries={curatedSceneRoute.recentEntries}
|
|
205
211
|
scenes={curatedSceneRoute.scenes}
|
|
206
212
|
isScenesLoading={curatedSceneRoute.isScenesLoading}
|
|
207
|
-
isItemsLoading={
|
|
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 {
|
|
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
|
-
? `${
|
|
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(
|
|
26
|
-
'openai-work/gpt-5.5',
|
|
27
|
-
|
|
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
|
|
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([], '
|
|
66
|
-
models: [],
|
|
67
|
-
draft: '
|
|
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';
|