@nextclaw/ui 0.15.14 → 0.15.16
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 +34 -0
- package/dist/assets/{api-DGG69ZcJ.js → api-D5UCLVE_.js} +3 -3
- package/dist/assets/app-presenter-provider-DcpcRgLn.js +106 -0
- package/dist/assets/{appearance-settings-page-Byr55LGb.js → appearance-settings-page-yJR6Kt9c.js} +1 -1
- package/dist/assets/{book-open-BhaS5BJv.js → book-open-WsEnuW9P.js} +1 -1
- package/dist/assets/{card-Bcr5YM8F.js → card-xVrS1b1T.js} +1 -1
- package/dist/assets/{channels-list-page-C_x83ZCT.js → channels-list-page-CAWuTHSM.js} +1 -1
- package/dist/assets/{chat-page-CEavSS6B.js → chat-page-CXYXMIQQ.js} +1 -1
- package/dist/assets/{config-split-page-RXAoLeNv.js → config-split-page-xj0UtMow.js} +1 -1
- package/dist/assets/{confirm-dialog-o_VPKvVD.js → confirm-dialog-e2YCUzt1.js} +1 -1
- package/dist/assets/{desktop-update-config-DEOMOsDN.js → desktop-update-config-D5NIaimo.js} +1 -1
- package/dist/assets/{dist-C87P4QWA.js → dist-Ca-jiFrK.js} +1 -1
- package/dist/assets/doc-browser-B2tNEKrJ.js +1 -0
- package/dist/assets/{doc-browser-DQ75iUkA.js → doc-browser-CZX6I6CU.js} +1 -1
- package/dist/assets/{doc-browser-context-tE0--wSa.js → doc-browser-context-CGz0LULR.js} +1 -1
- package/dist/assets/{eye-DL9QkrKg.js → eye-B30J471A.js} +1 -1
- package/dist/assets/{form-actions-J0tIaUdK.js → form-actions-CnSzu3WD.js} +1 -1
- package/dist/assets/{index-BFIZUZP3.js → index-6Esxu-Pp.js} +2 -2
- package/dist/assets/index-BzSgA8tR.css +1 -0
- package/dist/assets/loader-circle-DjwSRQHW.js +1 -0
- package/dist/assets/{logo-badge-BYTbZbOF.js → logo-badge-CISsltAo.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +1 -0
- package/dist/assets/{mcp-marketplace-page-COWlzeP1.js → mcp-marketplace-page-CPcPzMUQ.js} +1 -1
- package/dist/assets/{model-config-page-oNqnMXNY.js → model-config-page-D6ex3_NM.js} +1 -1
- package/dist/assets/{navigation-link-Dk6VdDkc.js → navigation-link-qPPUiLt0.js} +1 -1
- package/dist/assets/plus-C45awBb1.js +1 -0
- package/dist/assets/{popover-Txwrum65.js → popover-DHx55dbc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-B7rQM4b6.js → provider-scoped-model-input-1TJxAhx6.js} +1 -1
- package/dist/assets/{providers-config-page-CP-OnvLK.js → providers-config-page-BwHtHIPe.js} +1 -1
- package/dist/assets/{react-B0LbRnAE.js → react-DerwD2EA.js} +1 -1
- package/dist/assets/{refresh-cw-C84mMku5.js → refresh-cw-C7bynrWX.js} +1 -1
- package/dist/assets/remote-CPpTKDgX.js +1 -0
- package/dist/assets/rotate-cw-Bhx0XkQP.js +1 -0
- package/dist/assets/{runtime-config-page-CIGtssWi.js → runtime-config-page-CMLVC5UV.js} +1 -1
- package/dist/assets/{save-R_qyrHpa.js → save-CsRsFtHS.js} +1 -1
- package/dist/assets/{search-UKxiouNi.js → search-D0HtJIL-.js} +1 -1
- package/dist/assets/{search-config-page-DzkpSGFH.js → search-config-page-n1GElgij.js} +1 -1
- package/dist/assets/{secrets-config-page-DhoLYm7Z.js → secrets-config-page-DbUc3Ggi.js} +1 -1
- package/dist/assets/security-config-n2WHhHXV.js +1 -0
- package/dist/assets/{select-CYHT8P5S.js → select-C1zfp9zM.js} +1 -1
- package/dist/assets/{settings-2-DGh4bGSi.js → settings-2-Ba3cLSN2.js} +1 -1
- package/dist/assets/skeleton-DL5npsVL.js +1 -0
- package/dist/assets/{tabs-BCihm1zK.js → tabs-DmKgMKzb.js} +1 -1
- package/dist/assets/{tag-chip-De0iz4o2.js → tag-chip-2vwlVUwM.js} +1 -1
- package/dist/assets/{trash-2-Bo3DWiju.js → trash-2-CUpWjNHu.js} +1 -1
- package/dist/assets/{use-config-DCzwgChy.js → use-config-BvxDWV-M.js} +1 -1
- package/dist/assets/{use-confirm-dialog-WZ94CehU.js → use-confirm-dialog-CeUEE4p5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CZ-3pMne.js → use-viewport-layout-DaKJ-kap.js} +1 -1
- package/dist/assets/x-DGdYT7rH.js +1 -0
- package/dist/index.html +28 -28
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +2 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +11 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +4 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +3 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +64 -72
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +85 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +108 -257
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +52 -0
- package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +63 -0
- package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +134 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +5 -5
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +113 -32
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +3 -2
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +118 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list-skill-preview.container.test.tsx +170 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +18 -9
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +36 -23
- package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +90 -0
- package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +3 -5
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +51 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +9 -3
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +42 -8
- package/src/shared/lib/api/remote.types.ts +28 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/dist/assets/app-presenter-provider-Dp90n1Va.js +0 -106
- package/dist/assets/doc-browser-BuGh646I.js +0 -1
- package/dist/assets/index-B2YF-7Yw.css +0 -1
- package/dist/assets/loader-circle-CAnHHSrY.js +0 -1
- package/dist/assets/mcp-marketplace-page-CaUFv37E.js +0 -1
- package/dist/assets/plus-DjdTWPjQ.js +0 -1
- package/dist/assets/remote-_L4pTEaW.js +0 -1
- package/dist/assets/rotate-cw-DQJhDnjg.js +0 -1
- package/dist/assets/security-config-yraYoy9s.js +0 -1
- package/dist/assets/skeleton-BQuylYFO.js +0 -1
- package/dist/assets/x-NZxXyqW1.js +0 -1
|
@@ -181,6 +181,17 @@ vi.mock(
|
|
|
181
181
|
}),
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
+
vi.mock(
|
|
185
|
+
"@/features/chat/features/conversation/hooks/use-session-run-queue",
|
|
186
|
+
() => ({
|
|
187
|
+
useSessionRunQueue: () => ({
|
|
188
|
+
inputs: [],
|
|
189
|
+
isLoading: false,
|
|
190
|
+
removeQueuedInput: vi.fn(async () => null),
|
|
191
|
+
}),
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
184
195
|
vi.mock(
|
|
185
196
|
"@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils",
|
|
186
197
|
() => ({
|
|
@@ -387,6 +387,10 @@ function AttachmentSubmitHarness({ send }: { readonly send: AttachmentSubmitAgen
|
|
|
387
387
|
inputSnapshot,
|
|
388
388
|
inputQuery,
|
|
389
389
|
isRuntimeBlocked: false,
|
|
390
|
+
runQueue: {
|
|
391
|
+
inputs: [],
|
|
392
|
+
removeQueuedInput: async () => null,
|
|
393
|
+
},
|
|
390
394
|
selectedAgentId: 'main',
|
|
391
395
|
sessionKey: 'session-attachment',
|
|
392
396
|
resetComposer: inputActions.resetComposer,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "@/features/chat/features/conversation/hooks/use-session-conversation-controller";
|
|
26
26
|
import { useSessionConversationInputQuery } from "@/features/chat/features/conversation/hooks/use-session-conversation-input-query";
|
|
27
27
|
import { useSessionConversationInputState } from "@/features/chat/features/conversation/hooks/use-session-conversation-input-state";
|
|
28
|
+
import { useSessionRunQueue } from "@/features/chat/features/conversation/hooks/use-session-run-queue";
|
|
28
29
|
import {
|
|
29
30
|
SessionConversationInput,
|
|
30
31
|
type SessionConversationInputController,
|
|
@@ -192,6 +193,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
|
|
|
192
193
|
(state) => state.snapshot.selectedAgentId,
|
|
193
194
|
);
|
|
194
195
|
const agent = useNcpSessionConversation(sessionKey ?? undefined);
|
|
196
|
+
const runQueue = useSessionRunQueue(sessionKey);
|
|
195
197
|
const { inputActions, inputSnapshot } = useSessionConversationInputState(initialPrompt);
|
|
196
198
|
const inputQuery = useSessionConversationInputQuery({
|
|
197
199
|
sessionKey,
|
|
@@ -257,6 +259,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
|
|
|
257
259
|
inputQuery,
|
|
258
260
|
isRuntimeBlocked,
|
|
259
261
|
materializationContext,
|
|
262
|
+
runQueue,
|
|
260
263
|
selectedAgentId,
|
|
261
264
|
sessionKey,
|
|
262
265
|
onSessionMaterialized,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
-
import {
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
3
4
|
import type { NcpAgentSendEnvelope, NcpRunHandle } from '@nextclaw/ncp';
|
|
4
5
|
|
|
5
6
|
import { useSessionConversationController } from '@/features/chat/features/conversation/hooks/use-session-conversation-controller';
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
-
type TestAgentSend = (envelope: TestAgentSendEnvelope) => Promise<NcpRunHandle | null>;
|
|
8
|
+
type TestAgentSend = (envelope: NcpAgentSendEnvelope) => Promise<NcpRunHandle | null>;
|
|
9
9
|
|
|
10
10
|
function createTextNode(text: string) {
|
|
11
11
|
return {
|
|
@@ -26,34 +26,49 @@ function createRunHandle(overrides: Partial<NcpRunHandle> = {}): NcpRunHandle {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function
|
|
30
|
-
return vi.fn<TestAgentSend>(async () => createRunHandle());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function createAgent(params: {
|
|
34
|
-
isRunning: boolean;
|
|
35
|
-
send: ReturnType<typeof createSendMock>;
|
|
36
|
-
}) {
|
|
29
|
+
function createQueuedInput(): UiNcpSessionQueuedInputView {
|
|
37
30
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
id: 'queued-input-1',
|
|
32
|
+
sessionId: 'session-1',
|
|
33
|
+
enqueuedAt: '2026-07-05T10:00:00.000Z',
|
|
34
|
+
metadata: {},
|
|
35
|
+
message: {
|
|
36
|
+
id: 'user-message-queued',
|
|
37
|
+
sessionId: 'session-1',
|
|
38
|
+
role: 'user',
|
|
39
|
+
status: 'final',
|
|
40
|
+
timestamp: '2026-07-05T10:00:00.000Z',
|
|
41
|
+
parts: [{ type: 'text', text: 'queued task' }],
|
|
45
42
|
},
|
|
46
|
-
visibleMessages: [],
|
|
47
43
|
};
|
|
48
44
|
}
|
|
49
45
|
|
|
46
|
+
function createSendMock(handle: NcpRunHandle | null = createRunHandle()) {
|
|
47
|
+
return vi.fn<TestAgentSend>(async () => handle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
50
|
function createControllerParams(params: {
|
|
51
51
|
isRunning: boolean;
|
|
52
|
+
queuedInputs?: readonly UiNcpSessionQueuedInputView[];
|
|
52
53
|
send?: ReturnType<typeof createSendMock>;
|
|
53
54
|
}) {
|
|
54
|
-
const
|
|
55
|
+
const { isRunning, queuedInputs = [], send: requestedSend } = params;
|
|
56
|
+
const send = requestedSend ?? createSendMock();
|
|
57
|
+
const removeQueuedInput = vi.fn(async (id: string) =>
|
|
58
|
+
queuedInputs.find((item) => item.id === id) ?? null,
|
|
59
|
+
);
|
|
55
60
|
return {
|
|
56
|
-
agent:
|
|
61
|
+
agent: {
|
|
62
|
+
abort: vi.fn(),
|
|
63
|
+
isHydrating: false,
|
|
64
|
+
isRunning,
|
|
65
|
+
isSending: false,
|
|
66
|
+
send,
|
|
67
|
+
snapshot: {
|
|
68
|
+
activeRun: isRunning ? { sessionId: 'session-1' } : null,
|
|
69
|
+
},
|
|
70
|
+
visibleMessages: [],
|
|
71
|
+
},
|
|
57
72
|
inputQuery: {
|
|
58
73
|
defaultModel: 'test-model',
|
|
59
74
|
defaultProjectRoot: null,
|
|
@@ -90,6 +105,10 @@ function createControllerParams(params: {
|
|
|
90
105
|
text: 'next task',
|
|
91
106
|
},
|
|
92
107
|
isRuntimeBlocked: false,
|
|
108
|
+
runQueue: {
|
|
109
|
+
inputs: queuedInputs,
|
|
110
|
+
removeQueuedInput,
|
|
111
|
+
},
|
|
93
112
|
selectedAgentId: 'main',
|
|
94
113
|
sessionKey: 'session-1',
|
|
95
114
|
resetComposer: vi.fn(),
|
|
@@ -98,84 +117,57 @@ function createControllerParams(params: {
|
|
|
98
117
|
};
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
describe('useSessionConversationController
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
it('queues a draft while the agent is running and sends it when the run becomes idle', async () => {
|
|
107
|
-
vi.useFakeTimers({ toFake: ['Date'] });
|
|
108
|
-
vi.setSystemTime(new Date('2026-07-05T10:00:00.000Z'));
|
|
109
|
-
const send = createSendMock();
|
|
110
|
-
const { result, rerender } = renderHook(
|
|
111
|
-
({ isRunning }) => useSessionConversationController(createControllerParams({ isRunning, send })),
|
|
112
|
-
{ initialProps: { isRunning: true } },
|
|
113
|
-
);
|
|
120
|
+
describe('useSessionConversationController backend run queue', () => {
|
|
121
|
+
it('submits immediately while the session is running so the backend can enqueue it', async () => {
|
|
122
|
+
const send = createSendMock(createRunHandle({ runId: null }));
|
|
123
|
+
const params = createControllerParams({ isRunning: true, send });
|
|
124
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
114
125
|
|
|
115
126
|
await act(async () => {
|
|
116
127
|
await result.current.send();
|
|
117
128
|
});
|
|
118
129
|
|
|
119
|
-
expect(send).
|
|
120
|
-
expect(result.current.queuedInputs).toEqual([{ id: expect.any(String), preview: 'next task' }]);
|
|
121
|
-
|
|
122
|
-
vi.setSystemTime(new Date('2026-07-05T10:01:00.000Z'));
|
|
123
|
-
rerender({ isRunning: false });
|
|
124
|
-
|
|
125
|
-
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
130
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
126
131
|
expect(send.mock.calls[0]?.[0]).toMatchObject({
|
|
132
|
+
sessionId: 'session-1',
|
|
127
133
|
message: expect.objectContaining({
|
|
128
134
|
parts: [{ type: 'text', text: 'next task' }],
|
|
129
|
-
timestamp: '2026-07-05T10:01:00.000Z',
|
|
130
135
|
}),
|
|
131
|
-
sessionId: 'session-1',
|
|
132
136
|
});
|
|
133
|
-
expect(
|
|
137
|
+
expect(params.resetComposer).toHaveBeenCalledTimes(1);
|
|
134
138
|
});
|
|
135
139
|
|
|
136
|
-
it('returns a
|
|
137
|
-
const
|
|
138
|
-
const params = createControllerParams({ isRunning: true,
|
|
140
|
+
it('projects the backend queue and returns a removed item to the composer for editing', async () => {
|
|
141
|
+
const queuedInput = createQueuedInput();
|
|
142
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
139
143
|
const { result } = renderHook(() => useSessionConversationController(params));
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const queuedId = result.current.queuedInputs[0]?.id;
|
|
145
|
-
expect(queuedId).toBeTruthy();
|
|
146
|
-
|
|
145
|
+
expect(result.current.queuedInputs).toEqual([
|
|
146
|
+
{ id: queuedInput.id, preview: 'queued task' },
|
|
147
|
+
]);
|
|
147
148
|
act(() => {
|
|
148
|
-
result.current.editQueuedInput(
|
|
149
|
+
result.current.editQueuedInput(queuedInput.id);
|
|
149
150
|
});
|
|
150
151
|
|
|
151
|
-
expect(
|
|
152
|
-
expect(params.restoreComposer).toHaveBeenCalledWith({
|
|
152
|
+
await waitFor(() => expect(params.runQueue.removeQueuedInput).toHaveBeenCalledWith(queuedInput.id));
|
|
153
|
+
expect(params.restoreComposer).toHaveBeenCalledWith(expect.objectContaining({
|
|
153
154
|
attachments: [],
|
|
154
|
-
nodes: [createTextNode('next task')],
|
|
155
155
|
selectedSkills: [],
|
|
156
156
|
skillRecords: [],
|
|
157
|
-
text: '
|
|
158
|
-
});
|
|
159
|
-
expect(send).not.toHaveBeenCalled();
|
|
157
|
+
text: 'queued task',
|
|
158
|
+
}));
|
|
160
159
|
});
|
|
161
160
|
|
|
162
|
-
it('deletes
|
|
163
|
-
const
|
|
164
|
-
const params = createControllerParams({ isRunning: true,
|
|
161
|
+
it('deletes through the backend queue owner without restoring the composer', async () => {
|
|
162
|
+
const queuedInput = createQueuedInput();
|
|
163
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
165
164
|
const { result } = renderHook(() => useSessionConversationController(params));
|
|
166
165
|
|
|
167
|
-
await act(async () => {
|
|
168
|
-
await result.current.send();
|
|
169
|
-
});
|
|
170
|
-
const queuedId = result.current.queuedInputs[0]?.id;
|
|
171
|
-
expect(queuedId).toBeTruthy();
|
|
172
|
-
|
|
173
166
|
act(() => {
|
|
174
|
-
result.current.deleteQueuedInput(
|
|
167
|
+
result.current.deleteQueuedInput(queuedInput.id);
|
|
175
168
|
});
|
|
176
169
|
|
|
177
|
-
expect(
|
|
170
|
+
await waitFor(() => expect(params.runQueue.removeQueuedInput).toHaveBeenCalledWith(queuedInput.id));
|
|
178
171
|
expect(params.restoreComposer).not.toHaveBeenCalled();
|
|
179
|
-
expect(send).not.toHaveBeenCalled();
|
|
180
172
|
});
|
|
181
173
|
});
|
package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { eventKeys, type UiNcpSessionQueuedInputsView } from '@nextclaw/client-sdk';
|
|
6
|
+
|
|
7
|
+
import { useSessionRunQueue } from '@/features/chat/features/conversation/hooks/use-session-run-queue';
|
|
8
|
+
import { nextclawClient } from '@/shared/lib/api';
|
|
9
|
+
|
|
10
|
+
function createQueue(sessionId: string, text: string): UiNcpSessionQueuedInputsView {
|
|
11
|
+
return {
|
|
12
|
+
sessionId,
|
|
13
|
+
inputs: [{
|
|
14
|
+
id: `queued-${sessionId}`,
|
|
15
|
+
sessionId,
|
|
16
|
+
enqueuedAt: '2026-07-22T10:00:00.000Z',
|
|
17
|
+
metadata: {},
|
|
18
|
+
message: {
|
|
19
|
+
id: `message-${sessionId}`,
|
|
20
|
+
sessionId,
|
|
21
|
+
role: 'user',
|
|
22
|
+
status: 'final',
|
|
23
|
+
timestamp: '2026-07-22T10:00:00.000Z',
|
|
24
|
+
parts: [{ type: 'text', text }],
|
|
25
|
+
},
|
|
26
|
+
}],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createWrapper() {
|
|
31
|
+
const queryClient = new QueryClient({
|
|
32
|
+
defaultOptions: { queries: { retry: false } },
|
|
33
|
+
});
|
|
34
|
+
return function Wrapper({ children }: { readonly children: ReactNode }) {
|
|
35
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('useSessionRunQueue', () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keys backend queue data by session instead of sharing one frontend queue', async () => {
|
|
45
|
+
vi.spyOn(nextclawClient.sessions, 'listQueuedInputs').mockImplementation(async (sessionId) =>
|
|
46
|
+
createQueue(sessionId, sessionId === 'session-1' ? 'first session' : 'second session'),
|
|
47
|
+
);
|
|
48
|
+
const { result, rerender } = renderHook(
|
|
49
|
+
({ sessionKey }) => useSessionRunQueue(sessionKey),
|
|
50
|
+
{ initialProps: { sessionKey: 'session-1' }, wrapper: createWrapper() },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await waitFor(() => expect(result.current.inputs[0]?.message.parts[0]).toMatchObject({ text: 'first session' }));
|
|
54
|
+
rerender({ sessionKey: 'session-2' });
|
|
55
|
+
await waitFor(() => expect(result.current.inputs[0]?.message.parts[0]).toMatchObject({ text: 'second session' }));
|
|
56
|
+
expect(nextclawClient.sessions.listQueuedInputs).toHaveBeenCalledWith('session-1');
|
|
57
|
+
expect(nextclawClient.sessions.listQueuedInputs).toHaveBeenCalledWith('session-2');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('refetches only when the matching session queue changes', async () => {
|
|
61
|
+
const listQueuedInputs = vi.spyOn(nextclawClient.sessions, 'listQueuedInputs')
|
|
62
|
+
.mockResolvedValue(createQueue('session-1', 'queued'));
|
|
63
|
+
const { unmount } = renderHook(
|
|
64
|
+
() => useSessionRunQueue('session-1'),
|
|
65
|
+
{ wrapper: createWrapper() },
|
|
66
|
+
);
|
|
67
|
+
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(1));
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
nextclawClient.eventBus.emit(eventKeys.sessionRunQueueUpdated, {
|
|
71
|
+
sessionKey: 'session-2',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
75
|
+
expect(listQueuedInputs).toHaveBeenCalledTimes(1);
|
|
76
|
+
|
|
77
|
+
act(() => {
|
|
78
|
+
nextclawClient.eventBus.emit(eventKeys.sessionRunQueueUpdated, {
|
|
79
|
+
sessionKey: 'session-1',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
await waitFor(() => expect(listQueuedInputs).toHaveBeenCalledTimes(2));
|
|
83
|
+
unmount();
|
|
84
|
+
});
|
|
85
|
+
});
|