@nextclaw/ui 0.15.6 → 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 +17 -0
- package/dist/assets/{api-Dyb5rgh0.js → api-V8m62-13.js} +1 -1
- package/dist/assets/{app-presenter-provider-7jV0nPiL.js → app-presenter-provider-Bzj9h_ZS.js} +21 -21
- package/dist/assets/{appearance-settings-page-DFm3BbEb.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-L6LCuqOr.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-DOaPKNJA.js → channels-list-page-CYIqMliJ.js} +1 -1
- package/dist/assets/{chat-page-Cw9uDhD_.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-CqlqjnCK.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-bAp8NU93.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/{desktop-update-config-2SjMfCMl.js → desktop-update-config-B-q0OhxO.js} +1 -1
- package/dist/assets/{dist-CrtbGLlh.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-BmnQa30-.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-BlpUUFrR.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-BhZZNXD8.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-B9Uoq5J7.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-K5knauEM.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-BIYZZWJv.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-CKprWFeM.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-BQ-XFZod.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-BfA7A3n4.js → mcp-marketplace-page-CGVu7mfL.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-BbWbW_qM.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-CX9zFsuc.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-B-9_DtIP.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/{providers-config-page-xwIjGur2.js → providers-config-page-DXsoyMrh.js} +1 -1
- package/dist/assets/{react-DsgudwpT.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-kXdvbhFf.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-DTsi7czJ.js → runtime-config-page-eYrrKBrA.js} +1 -1
- package/dist/assets/{save-C9qpz2_2.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-_kEi29qo.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-H7ug8f4D.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-C1_6rINu.js → secrets-config-page-DwzKQJ76.js} +1 -1
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-MJKhKAWR.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-DA3tJuew.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D_y7zXJz.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-0uTLUhm9.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-h44XPocd.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-bRZ04q3C.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-BRpSE-Wt.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/{use-config-CwDEAl6f.js → use-config-CY4uF2Ou.js} +1 -1
- package/dist/assets/{use-confirm-dialog-BbnWw-8c.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-CqgIMDQL.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 +13 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +18 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +47 -2
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +63 -16
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +54 -4
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +4 -4
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +1 -3
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +110 -27
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +17 -1
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +17 -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 +55 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
- 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/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/shared/lib/i18n/locales/en-US/chat.json +5 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +1 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +1 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/dist/assets/doc-browser-A5IRyDXQ2.js +0 -1
- package/dist/assets/index-BVn702m4.css +0 -1
- package/dist/assets/index-CFxJ1Fkg.js +0 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +0 -1
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +0 -1
- package/dist/assets/plus-TmFdwO3P.js +0 -1
- package/dist/assets/remote-B9F2uCk9.js +0 -1
- package/dist/assets/rotate-cw-DEc6aihr.js +0 -1
- package/dist/assets/security-config-Cr2ERKLu.js +0 -1
- package/dist/assets/x-DdEDL6ju.js +0 -1
|
@@ -92,4 +92,59 @@ describe("useHydratedNcpAgent", () => {
|
|
|
92
92
|
expect(mocks.stream).toHaveBeenCalledWith({ sessionId: "session-b" });
|
|
93
93
|
expect(mocks.stream).toHaveBeenCalledTimes(2);
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
it("keeps a selected session hydrated when an earlier empty-session reset finishes late", async () => {
|
|
97
|
+
const stopResolvers: Array<() => void> = [];
|
|
98
|
+
const client = {
|
|
99
|
+
stop: mocks.stop.mockImplementation(
|
|
100
|
+
() => new Promise<void>((resolve) => stopResolvers.push(resolve)),
|
|
101
|
+
),
|
|
102
|
+
stream: mocks.stream.mockResolvedValue(undefined),
|
|
103
|
+
subscribe: vi.fn(() => () => {}),
|
|
104
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
105
|
+
const historyMessage = {
|
|
106
|
+
id: "message-1",
|
|
107
|
+
sessionId: "session-mobile",
|
|
108
|
+
role: "user",
|
|
109
|
+
status: "final",
|
|
110
|
+
parts: [{ type: "text", text: "Existing history" }],
|
|
111
|
+
timestamp: "2026-07-16T00:00:00.000Z",
|
|
112
|
+
} as const;
|
|
113
|
+
const loadSeed = vi.fn().mockResolvedValue({
|
|
114
|
+
messages: [historyMessage],
|
|
115
|
+
status: "idle",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { result, rerender } = renderHook(
|
|
119
|
+
({ sessionId }: { sessionId: string | undefined }) =>
|
|
120
|
+
useHydratedNcpAgent({
|
|
121
|
+
sessionId,
|
|
122
|
+
client: client as never,
|
|
123
|
+
loadSeed,
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
initialProps: {
|
|
127
|
+
sessionId: undefined as string | undefined,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await waitFor(() => expect(stopResolvers).toHaveLength(1));
|
|
133
|
+
rerender({ sessionId: "session-mobile" });
|
|
134
|
+
await waitFor(() => expect(stopResolvers).toHaveLength(2));
|
|
135
|
+
|
|
136
|
+
await act(async () => {
|
|
137
|
+
stopResolvers[1]?.();
|
|
138
|
+
await Promise.resolve();
|
|
139
|
+
});
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(result.current.visibleMessages).toEqual([historyMessage]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
stopResolvers[0]?.();
|
|
146
|
+
await Promise.resolve();
|
|
147
|
+
});
|
|
148
|
+
expect(result.current.visibleMessages).toEqual([historyMessage]);
|
|
149
|
+
});
|
|
95
150
|
});
|
package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx
CHANGED
|
@@ -235,7 +235,7 @@ export function ChatSessionTitleSwitcher({
|
|
|
235
235
|
(state) => state.isSidebarCollapsed,
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
-
if (layoutMode
|
|
238
|
+
if (layoutMode === "desktop" && !isSidebarCollapsed) {
|
|
239
239
|
return <ChatSessionTitle title={title} />;
|
|
240
240
|
}
|
|
241
241
|
|
|
@@ -20,6 +20,7 @@ const destroyPresentationMock = vi.fn();
|
|
|
20
20
|
type RenderWorkspaceFilePreviewOptions = {
|
|
21
21
|
file?: Partial<ChatWorkspaceFileTab>;
|
|
22
22
|
onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
|
|
23
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
23
24
|
refreshVersion?: number;
|
|
24
25
|
sessionProjectRoot?: string | null;
|
|
25
26
|
sessionWorkingDir?: string | null;
|
|
@@ -104,6 +105,7 @@ function mockTextRead(overrides: TextReadDataOverrides = {}) {
|
|
|
104
105
|
function renderWorkspaceFilePreview({
|
|
105
106
|
file,
|
|
106
107
|
onFileOpen = vi.fn(),
|
|
108
|
+
onHtmlContentHeightChange,
|
|
107
109
|
refreshVersion,
|
|
108
110
|
sessionProjectRoot = "/tmp",
|
|
109
111
|
sessionWorkingDir = "/tmp",
|
|
@@ -116,6 +118,7 @@ function renderWorkspaceFilePreview({
|
|
|
116
118
|
sessionWorkingDir={sessionWorkingDir}
|
|
117
119
|
refreshVersion={refreshVersion}
|
|
118
120
|
showBreadcrumbs={showBreadcrumbs}
|
|
121
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
119
122
|
onFileOpen={onFileOpen}
|
|
120
123
|
/>,
|
|
121
124
|
);
|
|
@@ -316,9 +319,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
316
319
|
});
|
|
317
320
|
|
|
318
321
|
expect(screen.getByTestId(testId)).toBeTruthy();
|
|
319
|
-
expect(
|
|
320
|
-
screen.queryByText(t("chatWorkspacePreviewTruncated")),
|
|
321
|
-
).toBeNull();
|
|
322
|
+
expect(screen.queryByText(t("chatWorkspacePreviewTruncated"))).toBeNull();
|
|
322
323
|
},
|
|
323
324
|
);
|
|
324
325
|
});
|
|
@@ -751,9 +752,7 @@ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
|
|
|
751
752
|
});
|
|
752
753
|
const indexButton = screen.getByRole("button", { name: "index.ts" });
|
|
753
754
|
fireEvent.click(componentsTreeItem.querySelector("button")!);
|
|
754
|
-
expect(
|
|
755
|
-
componentsTreeItem.getAttribute("aria-expanded"),
|
|
756
|
-
).toBe("true");
|
|
755
|
+
expect(componentsTreeItem.getAttribute("aria-expanded")).toBe("true");
|
|
757
756
|
expect(onFileOpen).not.toHaveBeenCalled();
|
|
758
757
|
|
|
759
758
|
fireEvent.click(indexButton);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { WorkspaceFileContentPreview } from "@/features/chat/features/workspace/components/workspace-file-content-preview";
|
|
4
|
+
|
|
5
|
+
const defaultResizeObserver = globalThis.ResizeObserver;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.stubGlobal("ResizeObserver", defaultResizeObserver);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("reports same-origin HTML height changes without replacing the iframe", () => {
|
|
12
|
+
const observe = vi.fn();
|
|
13
|
+
const disconnect = vi.fn();
|
|
14
|
+
let notifyResize: ResizeObserverCallback = () => undefined;
|
|
15
|
+
class HeightResizeObserver {
|
|
16
|
+
constructor(callback: ResizeObserverCallback) {
|
|
17
|
+
notifyResize = callback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
observe = observe;
|
|
21
|
+
|
|
22
|
+
unobserve = vi.fn();
|
|
23
|
+
|
|
24
|
+
disconnect = disconnect;
|
|
25
|
+
}
|
|
26
|
+
vi.stubGlobal("ResizeObserver", HeightResizeObserver);
|
|
27
|
+
const onHtmlContentHeightChange = vi.fn();
|
|
28
|
+
const { unmount } = render(
|
|
29
|
+
<WorkspaceFileContentPreview
|
|
30
|
+
contentUrl="/api/server-paths/content?path=%2Ftmp%2Fexample.html"
|
|
31
|
+
kind="html"
|
|
32
|
+
label="example.html"
|
|
33
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
const frame = screen.getByTestId(
|
|
37
|
+
"workspace-html-preview",
|
|
38
|
+
) as HTMLIFrameElement;
|
|
39
|
+
const iframeDocument = document.implementation.createHTMLDocument();
|
|
40
|
+
Object.defineProperty(frame, "contentDocument", {
|
|
41
|
+
configurable: true,
|
|
42
|
+
value: iframeDocument,
|
|
43
|
+
});
|
|
44
|
+
const { documentElement } = iframeDocument;
|
|
45
|
+
Object.defineProperty(documentElement, "scrollHeight", {
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: 640,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
fireEvent.load(frame);
|
|
51
|
+
|
|
52
|
+
expect(onHtmlContentHeightChange).toHaveBeenLastCalledWith(640);
|
|
53
|
+
expect(observe).toHaveBeenCalledWith(documentElement);
|
|
54
|
+
expect(screen.getByTestId("workspace-html-preview")).toBe(frame);
|
|
55
|
+
|
|
56
|
+
Object.defineProperty(documentElement, "scrollHeight", {
|
|
57
|
+
configurable: true,
|
|
58
|
+
value: 900,
|
|
59
|
+
});
|
|
60
|
+
notifyResize([], {} as ResizeObserver);
|
|
61
|
+
|
|
62
|
+
expect(onHtmlContentHeightChange).toHaveBeenLastCalledWith(900);
|
|
63
|
+
expect(screen.getByTestId("workspace-html-preview")).toBe(frame);
|
|
64
|
+
|
|
65
|
+
unmount();
|
|
66
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -180,6 +180,7 @@ function WorkspacePreviewBody({
|
|
|
180
180
|
directoryQuery,
|
|
181
181
|
fileBasePath,
|
|
182
182
|
onFileOpen,
|
|
183
|
+
onHtmlContentHeightChange,
|
|
183
184
|
previewBlock,
|
|
184
185
|
previewKind,
|
|
185
186
|
previewViewer,
|
|
@@ -194,6 +195,7 @@ function WorkspacePreviewBody({
|
|
|
194
195
|
directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
|
|
195
196
|
fileBasePath: string | null;
|
|
196
197
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
198
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
197
199
|
previewBlock: ChatFileOperationBlockViewModel | null;
|
|
198
200
|
previewKind: "text" | "markdown" | "binary";
|
|
199
201
|
previewViewer: "source" | "rendered" | null;
|
|
@@ -208,6 +210,7 @@ function WorkspacePreviewBody({
|
|
|
208
210
|
contentUrl={contentUrl}
|
|
209
211
|
kind={contentUrlKind}
|
|
210
212
|
label={contentLabel}
|
|
213
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
211
214
|
/>
|
|
212
215
|
);
|
|
213
216
|
}
|
|
@@ -291,6 +294,7 @@ type ChatSessionWorkspaceFilePreviewProps = {
|
|
|
291
294
|
sessionProjectRoot: string | null;
|
|
292
295
|
sessionWorkingDir: string | null;
|
|
293
296
|
showBreadcrumbs?: boolean;
|
|
297
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
294
298
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
295
299
|
};
|
|
296
300
|
|
|
@@ -300,6 +304,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
300
304
|
sessionProjectRoot,
|
|
301
305
|
sessionWorkingDir,
|
|
302
306
|
showBreadcrumbs = true,
|
|
307
|
+
onHtmlContentHeightChange,
|
|
303
308
|
onFileOpen,
|
|
304
309
|
}: ChatSessionWorkspaceFilePreviewProps) {
|
|
305
310
|
const isPreviewMode = file.viewMode === "preview";
|
|
@@ -384,7 +389,8 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
384
389
|
previewQuery?.data?.startLine,
|
|
385
390
|
previewText,
|
|
386
391
|
]);
|
|
387
|
-
const isTextPreviewTruncated =
|
|
392
|
+
const isTextPreviewTruncated =
|
|
393
|
+
!contentUrl && Boolean(previewQuery?.data?.truncated);
|
|
388
394
|
const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
|
|
389
395
|
const breadcrumb = useMemo(
|
|
390
396
|
() =>
|
|
@@ -394,12 +400,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
394
400
|
sessionProjectRoot: breadcrumbBasePath,
|
|
395
401
|
truncated: isTextPreviewTruncated,
|
|
396
402
|
}),
|
|
397
|
-
[
|
|
398
|
-
breadcrumbBasePath,
|
|
399
|
-
isTextPreviewTruncated,
|
|
400
|
-
previewPathKind,
|
|
401
|
-
resolvedPath,
|
|
402
|
-
],
|
|
403
|
+
[breadcrumbBasePath, isTextPreviewTruncated, previewPathKind, resolvedPath],
|
|
403
404
|
);
|
|
404
405
|
|
|
405
406
|
return (
|
|
@@ -422,6 +423,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
422
423
|
directoryQuery={directoryQuery}
|
|
423
424
|
fileBasePath={sessionWorkingDir}
|
|
424
425
|
onFileOpen={onFileOpen}
|
|
426
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
425
427
|
previewBlock={previewBlock}
|
|
426
428
|
previewKind={previewKind}
|
|
427
429
|
previewViewer={previewViewer}
|
|
@@ -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
|
/>
|
|
@@ -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
|