@nextclaw/ui 0.12.25 → 0.12.27
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 +74 -0
- package/dist/assets/{channels-list-page-FJDuPwU6.js → channels-list-page-DkPvpAqc.js} +1 -1
- package/dist/assets/chat-page-b7Zf32fF.js +1 -0
- package/dist/assets/{desktop-kk7qvZ-v.js → desktop-DVUbOWbR.js} +1 -1
- package/dist/assets/index-DmWo8dX2.css +1 -0
- package/dist/assets/{index-D-AAMKCt.js → index-DqJ3CYwi.js} +34 -37
- package/dist/assets/marketplace-page-BVqFjnEB.js +105 -0
- package/dist/assets/marketplace-page-DkQ2hTs1.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DIq_SpMe.js → mcp-marketplace-page-BOYJO0kp.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DSML7NN0.js +1 -0
- package/dist/assets/{model-config-Bc6VVnxy.js → model-config-Bg2yycmn.js} +1 -1
- package/dist/assets/{providers-list-DN0tvISH.js → providers-list-DC1q3fvC.js} +1 -1
- package/dist/assets/{runtime-config-page-CRWOwBbl.js → runtime-config-page-q-nC0C5i.js} +1 -1
- package/dist/assets/{search-config-C4c1yZSP.js → search-config-CcKHif8O.js} +1 -1
- package/dist/assets/{secrets-config-zAF30YfO.js → secrets-config-DSg6O92a.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-Cvz8ZteY.js → use-infinite-scroll-loader-DF2e6nQ2.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +9 -9
- package/src/features/agents/components/agents-page.test.tsx +1 -1
- package/src/features/agents/components/agents-page.tsx +1 -1
- package/src/features/chat/components/chat-session-workspace-panel.tsx +31 -45
- package/src/features/chat/components/chat-sidebar-session-item.tsx +7 -9
- package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +5 -2
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +2 -2
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +106 -78
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +172 -167
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +2 -2
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +147 -88
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -0
- package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
- package/src/features/chat/managers/ncp-chat-presenter.manager.ts +1 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +4 -1
- package/src/features/chat/stores/chat-input.store.ts +3 -1
- package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +54 -16
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +96 -24
- package/src/features/marketplace/components/marketplace-page.test.tsx +4 -0
- package/src/features/marketplace/components/marketplace-page.tsx +16 -12
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +14 -5
- package/src/platforms/desktop/components/desktop-app-shell.test.tsx +1 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +1 -1
- package/dist/assets/chat-page-D1fMNBrT.js +0 -1
- package/dist/assets/index-DnBeV2Xm.css +0 -1
- package/dist/assets/marketplace-page-BrCLRIc4.js +0 -105
- package/dist/assets/marketplace-page-odDpPYEs.js +0 -1
- package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +0 -1
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
type NcpEndpointEvent,
|
|
6
6
|
type NcpEndpointManifest,
|
|
7
7
|
type NcpEndpointSubscriber,
|
|
8
|
+
type NcpMessage,
|
|
9
|
+
type NcpStreamRequestPayload,
|
|
8
10
|
NcpEventType,
|
|
9
11
|
} from "@nextclaw/ncp";
|
|
10
12
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -31,9 +33,57 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
|
|
|
31
33
|
readonly stream = vi.fn(async () => {});
|
|
32
34
|
readonly abort = vi.fn(async () => {});
|
|
33
35
|
private listeners = new Set<NcpEndpointSubscriber>();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
36
|
+
|
|
37
|
+
emit = async (event: NcpEndpointEvent): Promise<void> => {
|
|
38
|
+
this.publish(event);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
subscribe = (listener: NcpEndpointSubscriber): (() => void) => {
|
|
42
|
+
this.listeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
this.listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
send = vi.fn(async (envelope: NcpAgentSendEnvelope) => ({
|
|
49
|
+
sessionId: "session-created",
|
|
50
|
+
userMessageId: envelope.message.id,
|
|
51
|
+
assistantMessageId: "assistant-1",
|
|
52
|
+
runId: "run-1",
|
|
53
|
+
...(envelope.correlationId ? { correlationId: envelope.correlationId } : {}),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
private publish = (event: NcpEndpointEvent): void => {
|
|
57
|
+
for (const listener of this.listeners) {
|
|
58
|
+
listener(event);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class ExistingSessionLiveClient implements NcpAgentClientEndpoint {
|
|
64
|
+
readonly manifest: NcpEndpointManifest = {
|
|
65
|
+
endpointKind: "agent",
|
|
66
|
+
endpointId: "existing-session-live-client",
|
|
67
|
+
version: "0.1.0",
|
|
68
|
+
supportsStreaming: true,
|
|
69
|
+
supportsAbort: true,
|
|
70
|
+
supportsProactiveMessages: false,
|
|
71
|
+
supportsLiveSessionStream: true,
|
|
72
|
+
supportedPartTypes: ["text"],
|
|
73
|
+
expectedLatency: "seconds",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
readonly start = vi.fn(async () => {});
|
|
77
|
+
readonly abort = vi.fn(async () => {});
|
|
78
|
+
private listeners = new Set<NcpEndpointSubscriber>();
|
|
79
|
+
private liveStreamActive = false;
|
|
80
|
+
|
|
81
|
+
stop = vi.fn(async () => {
|
|
82
|
+
this.liveStreamActive = false;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
stream = vi.fn(async (_payload: NcpStreamRequestPayload) => {
|
|
86
|
+
this.liveStreamActive = true;
|
|
37
87
|
});
|
|
38
88
|
|
|
39
89
|
emit = async (event: NcpEndpointEvent): Promise<void> => {
|
|
@@ -47,78 +97,59 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
|
|
|
47
97
|
};
|
|
48
98
|
};
|
|
49
99
|
|
|
50
|
-
send = vi.fn(async (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
role: "user",
|
|
59
|
-
status: "final",
|
|
60
|
-
parts: [{ type: "text", text: "hello" }],
|
|
61
|
-
timestamp: now,
|
|
100
|
+
send = vi.fn(async (envelope: NcpAgentSendEnvelope) => {
|
|
101
|
+
const events: NcpEndpointEvent[] = [
|
|
102
|
+
{
|
|
103
|
+
type: NcpEventType.RunStarted,
|
|
104
|
+
payload: {
|
|
105
|
+
sessionId: "session-existing",
|
|
106
|
+
messageId: "assistant-1",
|
|
107
|
+
runId: "run-1",
|
|
62
108
|
},
|
|
63
109
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
runId: "run-1",
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
await this.completionGate;
|
|
74
|
-
this.publish({
|
|
75
|
-
type: NcpEventType.MessageTextStart,
|
|
76
|
-
payload: {
|
|
77
|
-
sessionId: "session-created",
|
|
78
|
-
messageId: "assistant-1",
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
this.publish({
|
|
82
|
-
type: NcpEventType.MessageTextDelta,
|
|
83
|
-
payload: {
|
|
84
|
-
sessionId: "session-created",
|
|
85
|
-
messageId: "assistant-1",
|
|
86
|
-
delta: "done",
|
|
110
|
+
{
|
|
111
|
+
type: NcpEventType.MessageTextStart,
|
|
112
|
+
payload: {
|
|
113
|
+
sessionId: "session-existing",
|
|
114
|
+
messageId: "assistant-1",
|
|
115
|
+
},
|
|
87
116
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
{
|
|
118
|
+
type: NcpEventType.MessageTextDelta,
|
|
119
|
+
payload: {
|
|
120
|
+
sessionId: "session-existing",
|
|
121
|
+
messageId: "assistant-1",
|
|
122
|
+
delta: "done",
|
|
123
|
+
},
|
|
94
124
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
message: {
|
|
101
|
-
id: "assistant-1",
|
|
102
|
-
sessionId: "session-created",
|
|
103
|
-
role: "assistant",
|
|
104
|
-
status: "final",
|
|
105
|
-
parts: [{ type: "text", text: "done" }],
|
|
106
|
-
timestamp: now,
|
|
125
|
+
{
|
|
126
|
+
type: NcpEventType.MessageTextEnd,
|
|
127
|
+
payload: {
|
|
128
|
+
sessionId: "session-existing",
|
|
129
|
+
messageId: "assistant-1",
|
|
107
130
|
},
|
|
108
131
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
{
|
|
133
|
+
type: NcpEventType.RunFinished,
|
|
134
|
+
payload: {
|
|
135
|
+
sessionId: "session-existing",
|
|
136
|
+
runId: "run-1",
|
|
137
|
+
},
|
|
115
138
|
},
|
|
116
|
-
|
|
117
|
-
});
|
|
139
|
+
];
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
if (this.liveStreamActive) {
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
this.publish(event);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
sessionId: "session-existing",
|
|
148
|
+
userMessageId: envelope.message.id,
|
|
149
|
+
assistantMessageId: "assistant-1",
|
|
150
|
+
runId: "run-1",
|
|
151
|
+
};
|
|
152
|
+
});
|
|
122
153
|
|
|
123
154
|
private publish = (event: NcpEndpointEvent): void => {
|
|
124
155
|
for (const listener of this.listeners) {
|
|
@@ -127,12 +158,20 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
|
|
|
127
158
|
};
|
|
128
159
|
}
|
|
129
160
|
|
|
161
|
+
function readAssistantText(messages: readonly NcpMessage[]): string {
|
|
162
|
+
const assistant = messages.find((message) => message.role === "assistant");
|
|
163
|
+
return assistant?.parts
|
|
164
|
+
.filter((part) => part.type === "text")
|
|
165
|
+
.map((part) => part.text ?? "")
|
|
166
|
+
.join("") ?? "";
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
describe("useNcpAgentRuntime", () => {
|
|
131
170
|
beforeEach(() => {
|
|
132
171
|
vi.clearAllMocks();
|
|
133
172
|
});
|
|
134
173
|
|
|
135
|
-
it("
|
|
174
|
+
it("returns a command handle when a new root chat materializes a session id", async () => {
|
|
136
175
|
const client = new DeferredSendClient();
|
|
137
176
|
const manager = new DefaultNcpAgentConversationStateManager();
|
|
138
177
|
const envelope: NcpAgentSendEnvelope = {
|
|
@@ -150,31 +189,21 @@ describe("useNcpAgentRuntime", () => {
|
|
|
150
189
|
{ initialProps: { sessionId: undefined as string | undefined } },
|
|
151
190
|
);
|
|
152
191
|
|
|
153
|
-
let
|
|
154
|
-
act(() => {
|
|
155
|
-
|
|
192
|
+
let handle: Awaited<ReturnType<typeof result.current.send>> | null = null;
|
|
193
|
+
await act(async () => {
|
|
194
|
+
handle = await result.current.send(envelope);
|
|
156
195
|
});
|
|
157
196
|
|
|
158
|
-
|
|
159
|
-
|
|
197
|
+
expect(handle).toEqual({
|
|
198
|
+
sessionId: "session-created",
|
|
199
|
+
userMessageId: "user-1",
|
|
200
|
+
assistantMessageId: "assistant-1",
|
|
201
|
+
runId: "run-1",
|
|
160
202
|
});
|
|
161
|
-
|
|
162
|
-
rerender({ sessionId: "session-created" });
|
|
163
|
-
|
|
203
|
+
expect(result.current.visibleMessages).toEqual([]);
|
|
164
204
|
expect(client.stop).not.toHaveBeenCalled();
|
|
165
205
|
|
|
166
|
-
|
|
167
|
-
client.release();
|
|
168
|
-
await sendPromise;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
await waitFor(() => {
|
|
172
|
-
expect(result.current.snapshot.activeRun).toBeNull();
|
|
173
|
-
});
|
|
174
|
-
expect(result.current.visibleMessages.map((message) => message.id)).toEqual([
|
|
175
|
-
"user-1",
|
|
176
|
-
"assistant-1",
|
|
177
|
-
]);
|
|
206
|
+
rerender({ sessionId: "session-created" });
|
|
178
207
|
});
|
|
179
208
|
|
|
180
209
|
it("aborts by session id even before a hydrated active run reaches local state", async () => {
|
|
@@ -190,4 +219,34 @@ describe("useNcpAgentRuntime", () => {
|
|
|
190
219
|
|
|
191
220
|
expect(client.abort).toHaveBeenCalledWith({ sessionId: "session-running" });
|
|
192
221
|
});
|
|
222
|
+
|
|
223
|
+
it("uses the hydrated live stream as the only event source while sending to an existing session", async () => {
|
|
224
|
+
const client = new ExistingSessionLiveClient();
|
|
225
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
226
|
+
await client.stream({ sessionId: "session-existing" });
|
|
227
|
+
const { result } = renderHook(() =>
|
|
228
|
+
useNcpAgentRuntime({ sessionId: "session-existing", client, manager: manager as never }),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await act(async () => {
|
|
232
|
+
await result.current.send({
|
|
233
|
+
sessionId: "session-existing",
|
|
234
|
+
message: {
|
|
235
|
+
id: "user-1",
|
|
236
|
+
sessionId: "session-existing",
|
|
237
|
+
role: "user",
|
|
238
|
+
status: "final",
|
|
239
|
+
parts: [{ type: "text", text: "hello" }],
|
|
240
|
+
timestamp: now,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await waitFor(() => {
|
|
246
|
+
expect(result.current.snapshot.activeRun).toBeNull();
|
|
247
|
+
expect(readAssistantText(result.current.visibleMessages)).toBe("done");
|
|
248
|
+
});
|
|
249
|
+
expect(client.stop).not.toHaveBeenCalled();
|
|
250
|
+
expect(client.stream).toHaveBeenCalledTimes(1);
|
|
251
|
+
});
|
|
193
252
|
});
|
|
@@ -15,6 +15,7 @@ describe('NcpChatInputManager', () => {
|
|
|
15
15
|
composerNodes: [createChatComposerTextNode('hello from current thread')],
|
|
16
16
|
attachments: [],
|
|
17
17
|
selectedSkills: [],
|
|
18
|
+
composerFocusRequest: null,
|
|
18
19
|
selectedSessionType: 'native',
|
|
19
20
|
selectedModel: 'gpt-5',
|
|
20
21
|
selectedThinkingLevel: null,
|
|
@@ -209,4 +210,23 @@ describe('NcpChatInputManager', () => {
|
|
|
209
210
|
expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
|
|
210
211
|
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
211
212
|
});
|
|
213
|
+
|
|
214
|
+
it('creates and consumes one-shot composer focus requests', () => {
|
|
215
|
+
const manager = new NcpChatInputManager(
|
|
216
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
217
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[1],
|
|
218
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[2],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
manager.requestComposerFocusAtEnd();
|
|
222
|
+
|
|
223
|
+
const request = useChatInputStore.getState().snapshot.composerFocusRequest;
|
|
224
|
+
expect(request).toEqual({ id: 1, placement: 'end' });
|
|
225
|
+
|
|
226
|
+
manager.consumeComposerFocusRequest(999);
|
|
227
|
+
expect(useChatInputStore.getState().snapshot.composerFocusRequest).toEqual(request);
|
|
228
|
+
|
|
229
|
+
manager.consumeComposerFocusRequest(request!.id);
|
|
230
|
+
expect(useChatInputStore.getState().snapshot.composerFocusRequest).toBeNull();
|
|
231
|
+
});
|
|
212
232
|
});
|
|
@@ -132,6 +132,24 @@ export class NcpChatInputManager {
|
|
|
132
132
|
this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
requestComposerFocusAtEnd = () => {
|
|
136
|
+
const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
|
|
137
|
+
useChatInputStore.getState().setSnapshot({
|
|
138
|
+
composerFocusRequest: {
|
|
139
|
+
id: (currentRequest?.id ?? 0) + 1,
|
|
140
|
+
placement: 'end',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
consumeComposerFocusRequest = (requestId: number) => {
|
|
146
|
+
const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
|
|
147
|
+
if (currentRequest?.id !== requestId) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
useChatInputStore.getState().setSnapshot({ composerFocusRequest: null });
|
|
151
|
+
};
|
|
152
|
+
|
|
135
153
|
setComposerNodes = (next: SetStateAction<ChatComposerNode[]>) => {
|
|
136
154
|
const prev = useChatInputStore.getState().snapshot.composerNodes;
|
|
137
155
|
const value = this.resolveUpdateValue(prev, next);
|
|
@@ -281,7 +281,10 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
|
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
try {
|
|
284
|
-
await agent.send(envelope);
|
|
284
|
+
const handle = await agent.send(envelope);
|
|
285
|
+
if (!payload.sessionKey && handle?.sessionId) {
|
|
286
|
+
presenter.chatSessionListManager.materializeRootSessionRoute(handle.sessionId);
|
|
287
|
+
}
|
|
285
288
|
} catch (error) {
|
|
286
289
|
if (payload.restoreDraftOnError) {
|
|
287
290
|
if (payload.composerNodes && payload.composerNodes.length > 0) {
|
|
@@ -44,6 +44,7 @@ export type ChatInputSnapshot = {
|
|
|
44
44
|
skillRecords: SessionSkillEntryView[];
|
|
45
45
|
isSkillsLoading: boolean;
|
|
46
46
|
selectedSkills: string[];
|
|
47
|
+
composerFocusRequest: { id: number; placement: 'end' } | null;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
type ChatInputStore = {
|
|
@@ -75,7 +76,8 @@ const initialSnapshot: ChatInputSnapshot = {
|
|
|
75
76
|
sessionTypeUnavailable: false,
|
|
76
77
|
skillRecords: [],
|
|
77
78
|
isSkillsLoading: false,
|
|
78
|
-
selectedSkills: []
|
|
79
|
+
selectedSkills: [],
|
|
80
|
+
composerFocusRequest: null
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
export const useChatInputStore = create<ChatInputStore>((set) => ({
|
package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx
CHANGED
|
@@ -18,11 +18,27 @@ type ItemsQueryState = {
|
|
|
18
18
|
fetchNextPage?: () => Promise<unknown>;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
type ScenesQueryState = {
|
|
22
|
+
data?: {
|
|
23
|
+
scenes: Array<{
|
|
24
|
+
scene: string;
|
|
25
|
+
title: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
count?: number;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
isLoading: boolean;
|
|
31
|
+
isFetching: boolean;
|
|
32
|
+
isError: boolean;
|
|
33
|
+
error: Error | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
21
36
|
const mocks = vi.hoisted(() => ({
|
|
22
37
|
navigate: vi.fn(),
|
|
23
38
|
docOpen: vi.fn(),
|
|
24
39
|
routeParams: {} as { scene?: string },
|
|
25
40
|
itemsQuery: null as unknown as ItemsQueryState,
|
|
41
|
+
scenesQuery: null as unknown as ScenesQueryState,
|
|
26
42
|
}));
|
|
27
43
|
|
|
28
44
|
vi.mock("react-router-dom", async () => {
|
|
@@ -55,21 +71,7 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
|
|
|
55
71
|
|
|
56
72
|
vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
|
|
57
73
|
useMarketplaceItems: () => mocks.itemsQuery,
|
|
58
|
-
useMarketplaceSkillScenes: () =>
|
|
59
|
-
data: {
|
|
60
|
-
scenes: [
|
|
61
|
-
{
|
|
62
|
-
scene: "development-debugging",
|
|
63
|
-
title: "Development",
|
|
64
|
-
description: "Review, debug, analyze, and verify delivery work.",
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
isLoading: false,
|
|
69
|
-
isFetching: false,
|
|
70
|
-
isError: false,
|
|
71
|
-
error: null,
|
|
72
|
-
}),
|
|
74
|
+
useMarketplaceSkillScenes: () => mocks.scenesQuery,
|
|
73
75
|
useMarketplaceSkillSceneCounts: () => new Map([["development-debugging", 2]]),
|
|
74
76
|
useMarketplaceInstalled: () => ({
|
|
75
77
|
data: {
|
|
@@ -165,12 +167,32 @@ function createItemsQuery(items: MarketplaceItemSummary[]) {
|
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
function createScenesQuery(overrides: Partial<ScenesQueryState> = {}) {
|
|
171
|
+
return {
|
|
172
|
+
data: {
|
|
173
|
+
scenes: [
|
|
174
|
+
{
|
|
175
|
+
scene: "development-debugging",
|
|
176
|
+
title: "Development",
|
|
177
|
+
description: "Review, debug, analyze, and verify delivery work.",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
isLoading: false,
|
|
182
|
+
isFetching: false,
|
|
183
|
+
isError: false,
|
|
184
|
+
error: null,
|
|
185
|
+
...overrides,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
describe("Marketplace curated scene routes", () => {
|
|
169
190
|
beforeEach(() => {
|
|
170
191
|
mocks.navigate.mockReset();
|
|
171
192
|
mocks.docOpen.mockReset();
|
|
172
193
|
mocks.routeParams = {};
|
|
173
194
|
mocks.itemsQuery = createItemsQuery(createSceneItems());
|
|
195
|
+
mocks.scenesQuery = createScenesQuery();
|
|
174
196
|
});
|
|
175
197
|
|
|
176
198
|
it("opens curated goals through a scene route", async () => {
|
|
@@ -183,6 +205,7 @@ describe("Marketplace curated scene routes", () => {
|
|
|
183
205
|
expect(mocks.navigate).toHaveBeenCalledWith(
|
|
184
206
|
"/skills/scenes/development-debugging",
|
|
185
207
|
);
|
|
208
|
+
expect(screen.getByText("All Skills")).toBeTruthy();
|
|
186
209
|
expect(container.querySelector("input")?.getAttribute("value") ?? "").toBe("");
|
|
187
210
|
});
|
|
188
211
|
|
|
@@ -196,7 +219,7 @@ describe("Marketplace curated scene routes", () => {
|
|
|
196
219
|
expect(screen.getByRole("button", { name: "Back" })).toBeTruthy();
|
|
197
220
|
expect(screen.getByText("Code Review")).toBeTruthy();
|
|
198
221
|
expect(screen.queryByText("Calendar Sync")).toBeNull();
|
|
199
|
-
expect(screen.queryByText("
|
|
222
|
+
expect(screen.queryByText("All Skills")).toBeNull();
|
|
200
223
|
expect(screen.queryByPlaceholderText("Search skills...")).toBeNull();
|
|
201
224
|
|
|
202
225
|
await user.click(screen.getByRole("button", { name: "Back" }));
|
|
@@ -232,4 +255,19 @@ describe("Marketplace curated scene routes", () => {
|
|
|
232
255
|
|
|
233
256
|
expect(screen.getByTestId("marketplace-loading-more")).toBeTruthy();
|
|
234
257
|
});
|
|
258
|
+
|
|
259
|
+
it("keeps the shelf layout stable while scenes are still loading", () => {
|
|
260
|
+
mocks.scenesQuery = createScenesQuery({
|
|
261
|
+
data: undefined,
|
|
262
|
+
isLoading: true,
|
|
263
|
+
isFetching: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
render(<MarketplacePage forcedType="skills" />);
|
|
267
|
+
|
|
268
|
+
expect(screen.getByTestId("marketplace-scenes-skeleton")).toBeTruthy();
|
|
269
|
+
expect(screen.getByText("Recently updated")).toBeTruthy();
|
|
270
|
+
expect(screen.getByText("All Skills")).toBeTruthy();
|
|
271
|
+
expect(screen.queryByRole("button", { name: /Development/ })).toBeNull();
|
|
272
|
+
});
|
|
235
273
|
});
|