@steerable/agent-ui 0.2.5 → 0.3.0
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.
|
@@ -56,6 +56,11 @@ export interface UseChatStreamOptions {
|
|
|
56
56
|
export interface UseChatStreamReturn {
|
|
57
57
|
messages: ChatMessage[];
|
|
58
58
|
isStreaming: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Send a user message. Queue-safe (W6-2): if a turn is currently streaming
|
|
61
|
+
* the message is NOT dropped — it is appended to the follow-up queue and
|
|
62
|
+
* sent automatically as the next turn when the current one ends.
|
|
63
|
+
*/
|
|
59
64
|
sendUserMessage: (input: ChatStreamSendInput) => Promise<void>;
|
|
60
65
|
/**
|
|
61
66
|
* Steer the running turn with an extra user message. No-op resolving
|
|
@@ -64,6 +69,17 @@ export interface UseChatStreamReturn {
|
|
|
64
69
|
* consumes it at the next round boundary).
|
|
65
70
|
*/
|
|
66
71
|
steerUserMessage: (content: string) => Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Explicitly queue a message as a follow-up (W6-2): sent automatically as
|
|
74
|
+
* the next turn once the current turn finishes. When not streaming this is
|
|
75
|
+
* equivalent to `sendUserMessage`. Distinct from `steerUserMessage`, which
|
|
76
|
+
* redirects the *current* turn mid-flight.
|
|
77
|
+
*/
|
|
78
|
+
followUpUserMessage: (input: ChatStreamSendInput) => void;
|
|
79
|
+
/** Follow-up messages queued while streaming, in order (not yet sent). */
|
|
80
|
+
pendingFollowUps: ChatStreamSendInput[];
|
|
81
|
+
/** Remove a queued follow-up by index (e.g. the user withdraws it). */
|
|
82
|
+
removeFollowUp: (index: number) => void;
|
|
67
83
|
cancel: () => void;
|
|
68
84
|
/** Replace the message buffer (e.g. when switching chat). */
|
|
69
85
|
setMessages: (messages: ChatMessage[]) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatStream.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAER,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;OAQG;IACH,MAAM,EAAE,CACN,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,KAC/B,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,SAAS,EAAE,mBAAmB,CAAC;IAC/B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,WAAW,GAAG,IAAI,CAAC;IACjE;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC/C,mEAAmE;IACnE,aAAa,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CAC/C;AAsID,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,GAC5B,mBAAmB,
|
|
1
|
+
{"version":3,"file":"useChatStream.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAER,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;OAQG;IACH,MAAM,EAAE,CACN,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,KAC/B,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,SAAS,EAAE,mBAAmB,CAAC;IAC/B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,WAAW,GAAG,IAAI,CAAC;IACjE;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD;;;;;OAKG;IACH,mBAAmB,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC1D,0EAA0E;IAC1E,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IACxC,uEAAuE;IACvE,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC/C,mEAAmE;IACnE,aAAa,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CAC/C;AAsID,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,GAC5B,mBAAmB,CA+MrB"}
|
|
@@ -133,6 +133,14 @@ export function useChatStream(options) {
|
|
|
133
133
|
const isStreamingRef = useRef(false);
|
|
134
134
|
const cancelRef = useRef(null);
|
|
135
135
|
const [, forceRerender] = useReducer((x) => x + 1, 0);
|
|
136
|
+
// W6-2 follow-up queue: messages submitted while a turn is streaming. Kept
|
|
137
|
+
// in a ref (drained imperatively at turn end) with a version counter to
|
|
138
|
+
// re-render consumers that show the pending queue.
|
|
139
|
+
const followUpsRef = useRef([]);
|
|
140
|
+
const [followUpVersion, bumpFollowUpVersion] = useReducer((x) => x + 1, 0);
|
|
141
|
+
// Self-handle so a finishing turn can start the next queued turn without a
|
|
142
|
+
// circular useCallback dependency.
|
|
143
|
+
const sendRef = useRef();
|
|
136
144
|
const setStreaming = useCallback((next) => {
|
|
137
145
|
isStreamingRef.current = next;
|
|
138
146
|
forceRerender();
|
|
@@ -195,6 +203,10 @@ export function useChatStream(options) {
|
|
|
195
203
|
}, [options]);
|
|
196
204
|
const sendUserMessage = useCallback(async (input) => {
|
|
197
205
|
if (isStreamingRef.current) {
|
|
206
|
+
// W6-2: queue instead of dropping. The finishing turn's `finally`
|
|
207
|
+
// drains the queue and starts the next turn automatically.
|
|
208
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
209
|
+
bumpFollowUpVersion();
|
|
198
210
|
return;
|
|
199
211
|
}
|
|
200
212
|
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
@@ -216,9 +228,36 @@ export function useChatStream(options) {
|
|
|
216
228
|
finally {
|
|
217
229
|
cancelRef.current = null;
|
|
218
230
|
setStreaming(false);
|
|
231
|
+
// W6-2: drain the follow-up queue — send the next queued message as
|
|
232
|
+
// a fresh turn. isStreamingRef is already false here, so the nested
|
|
233
|
+
// call starts a new turn rather than re-queueing.
|
|
234
|
+
const next = followUpsRef.current.shift();
|
|
235
|
+
if (next !== undefined) {
|
|
236
|
+
bumpFollowUpVersion();
|
|
237
|
+
void sendRef.current?.(next);
|
|
238
|
+
}
|
|
219
239
|
}
|
|
220
240
|
}, [handleEvent, options.transport, setStreaming]);
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
sendRef.current = sendUserMessage;
|
|
243
|
+
}, [sendUserMessage]);
|
|
244
|
+
const followUpUserMessage = useCallback((input) => {
|
|
245
|
+
if (!isStreamingRef.current) {
|
|
246
|
+
void sendRef.current?.(input);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
250
|
+
bumpFollowUpVersion();
|
|
251
|
+
}, []);
|
|
252
|
+
const removeFollowUp = useCallback((index) => {
|
|
253
|
+
followUpsRef.current = followUpsRef.current.filter((_, i) => i !== index);
|
|
254
|
+
bumpFollowUpVersion();
|
|
255
|
+
}, []);
|
|
221
256
|
const cancel = useCallback(() => {
|
|
257
|
+
// Cancelling the current turn also drops queued follow-ups — an explicit
|
|
258
|
+
// stop means "don't keep going", so we don't auto-send what was queued.
|
|
259
|
+
followUpsRef.current = [];
|
|
260
|
+
bumpFollowUpVersion();
|
|
222
261
|
if (cancelRef.current) {
|
|
223
262
|
cancelRef.current();
|
|
224
263
|
cancelRef.current = null;
|
|
@@ -255,9 +294,25 @@ export function useChatStream(options) {
|
|
|
255
294
|
isStreaming: isStreamingRef.current,
|
|
256
295
|
sendUserMessage,
|
|
257
296
|
steerUserMessage,
|
|
297
|
+
followUpUserMessage,
|
|
298
|
+
// followUpVersion re-keys this memo so queue changes surface to the UI.
|
|
299
|
+
pendingFollowUps: followUpsRef.current,
|
|
300
|
+
removeFollowUp,
|
|
301
|
+
cancel,
|
|
302
|
+
setMessages,
|
|
303
|
+
appendMessage,
|
|
304
|
+
}),
|
|
305
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
306
|
+
[
|
|
307
|
+
state.messages,
|
|
308
|
+
sendUserMessage,
|
|
309
|
+
steerUserMessage,
|
|
310
|
+
followUpUserMessage,
|
|
311
|
+
removeFollowUp,
|
|
258
312
|
cancel,
|
|
259
313
|
setMessages,
|
|
260
314
|
appendMessage,
|
|
261
|
-
|
|
315
|
+
followUpVersion,
|
|
316
|
+
]);
|
|
262
317
|
}
|
|
263
318
|
//# sourceMappingURL=useChatStream.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatStream.js","sourceRoot":"","sources":["../../src/hooks/useChatStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useChatStream.js","sourceRoot":"","sources":["../../src/hooks/useChatStream.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Steerable framework Tier 4 — headless React hooks (useAgentSession, useChatStream, useToolCallStatus) and Tailwind-themed components (ChatPanel, MessageList, OrchestrationPlanCard, ToolCallRenderer, SSEStreamView) for embedding Steerable agents in any React app.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/storybook/",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"README.md"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@steerable/agent-protocol": "0.
|
|
60
|
+
"@steerable/agent-protocol": "0.3.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=18.0.0",
|
|
@@ -12,9 +12,36 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import type { SSEEvent } from '@steerable/agent-protocol';
|
|
13
13
|
import {
|
|
14
14
|
useChatStream,
|
|
15
|
+
type ChatStreamSendInput,
|
|
15
16
|
type ChatStreamTransport,
|
|
16
17
|
} from './useChatStream';
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A transport whose streams stay open until the test explicitly finishes them,
|
|
21
|
+
* so we can submit follow-ups mid-stream and watch the queue drain.
|
|
22
|
+
*/
|
|
23
|
+
function makeControllableTransport() {
|
|
24
|
+
const streams: Array<{
|
|
25
|
+
input: ChatStreamSendInput;
|
|
26
|
+
onEvent: (e: SSEEvent) => void;
|
|
27
|
+
finish: () => void;
|
|
28
|
+
}> = [];
|
|
29
|
+
const transport: ChatStreamTransport = {
|
|
30
|
+
stream: (input, onEvent) =>
|
|
31
|
+
new Promise<void>((resolve) => {
|
|
32
|
+
streams.push({
|
|
33
|
+
input,
|
|
34
|
+
onEvent,
|
|
35
|
+
finish: () => {
|
|
36
|
+
onEvent({ type: 'done' });
|
|
37
|
+
resolve();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
return { transport, streams };
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
function makeTransport(
|
|
19
46
|
script: SSEEvent[][],
|
|
20
47
|
): { transport: ChatStreamTransport; cancel: ReturnType<typeof vi.fn> } {
|
|
@@ -309,3 +336,129 @@ describe('steerUserMessage', () => {
|
|
|
309
336
|
});
|
|
310
337
|
});
|
|
311
338
|
});
|
|
339
|
+
|
|
340
|
+
describe('follow-up queue (W6-2)', () => {
|
|
341
|
+
it('sendUserMessage while streaming queues instead of dropping, then auto-sends next turn', async () => {
|
|
342
|
+
const { transport, streams } = makeControllableTransport();
|
|
343
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
344
|
+
|
|
345
|
+
// Turn 1 starts and stays open.
|
|
346
|
+
act(() => {
|
|
347
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
348
|
+
});
|
|
349
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
350
|
+
expect(result.current.isStreaming).toBe(true);
|
|
351
|
+
|
|
352
|
+
// Submit a second message mid-stream — must be queued, not dropped.
|
|
353
|
+
await act(async () => {
|
|
354
|
+
await result.current.sendUserMessage({ content: 'second' });
|
|
355
|
+
});
|
|
356
|
+
expect(streams).toHaveLength(1); // no new turn yet
|
|
357
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: 'second' }]);
|
|
358
|
+
|
|
359
|
+
// Finish turn 1 → the queued message auto-sends as turn 2.
|
|
360
|
+
await act(async () => {
|
|
361
|
+
streams[0]!.finish();
|
|
362
|
+
});
|
|
363
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
364
|
+
expect(streams[1]!.input.content).toBe('second');
|
|
365
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
366
|
+
|
|
367
|
+
await act(async () => {
|
|
368
|
+
streams[1]!.finish();
|
|
369
|
+
});
|
|
370
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
371
|
+
// Both user messages landed in the transcript.
|
|
372
|
+
const userTexts = result.current.messages
|
|
373
|
+
.filter((m) => m.role === 'user')
|
|
374
|
+
.map((m) => m.content);
|
|
375
|
+
expect(userTexts).toEqual(['first', 'second']);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('followUpUserMessage queues while streaming and sends immediately when idle', async () => {
|
|
379
|
+
const { transport, streams } = makeControllableTransport();
|
|
380
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
381
|
+
|
|
382
|
+
// Idle: followUpUserMessage behaves like a normal send.
|
|
383
|
+
await act(async () => {
|
|
384
|
+
result.current.followUpUserMessage({ content: 'idle' });
|
|
385
|
+
});
|
|
386
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
387
|
+
expect(streams[0]!.input.content).toBe('idle');
|
|
388
|
+
|
|
389
|
+
// Streaming: queues.
|
|
390
|
+
await act(async () => {
|
|
391
|
+
result.current.followUpUserMessage({ content: 'queued' });
|
|
392
|
+
});
|
|
393
|
+
expect(streams).toHaveLength(1);
|
|
394
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: 'queued' }]);
|
|
395
|
+
|
|
396
|
+
await act(async () => {
|
|
397
|
+
streams[0]!.finish();
|
|
398
|
+
});
|
|
399
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
400
|
+
expect(streams[1]!.input.content).toBe('queued');
|
|
401
|
+
await act(async () => {
|
|
402
|
+
streams[1]!.finish();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('removeFollowUp withdraws a queued message before it sends', async () => {
|
|
407
|
+
const { transport, streams } = makeControllableTransport();
|
|
408
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
409
|
+
|
|
410
|
+
act(() => {
|
|
411
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
412
|
+
});
|
|
413
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
414
|
+
|
|
415
|
+
await act(async () => {
|
|
416
|
+
result.current.followUpUserMessage({ content: 'keep' });
|
|
417
|
+
result.current.followUpUserMessage({ content: 'drop' });
|
|
418
|
+
});
|
|
419
|
+
expect(result.current.pendingFollowUps.map((m) => m.content)).toEqual(['keep', 'drop']);
|
|
420
|
+
|
|
421
|
+
await act(async () => {
|
|
422
|
+
result.current.removeFollowUp(1); // remove 'drop'
|
|
423
|
+
});
|
|
424
|
+
expect(result.current.pendingFollowUps.map((m) => m.content)).toEqual(['keep']);
|
|
425
|
+
|
|
426
|
+
await act(async () => {
|
|
427
|
+
streams[0]!.finish();
|
|
428
|
+
});
|
|
429
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
430
|
+
expect(streams[1]!.input.content).toBe('keep');
|
|
431
|
+
await act(async () => {
|
|
432
|
+
streams[1]!.finish();
|
|
433
|
+
});
|
|
434
|
+
// 'drop' never sent.
|
|
435
|
+
expect(streams).toHaveLength(2);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('cancel() clears the queue so nothing auto-sends afterwards', async () => {
|
|
439
|
+
const { transport, streams } = makeControllableTransport();
|
|
440
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
441
|
+
|
|
442
|
+
act(() => {
|
|
443
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
444
|
+
});
|
|
445
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
446
|
+
|
|
447
|
+
await act(async () => {
|
|
448
|
+
result.current.followUpUserMessage({ content: 'queued' });
|
|
449
|
+
});
|
|
450
|
+
expect(result.current.pendingFollowUps).toHaveLength(1);
|
|
451
|
+
|
|
452
|
+
await act(async () => {
|
|
453
|
+
result.current.cancel();
|
|
454
|
+
});
|
|
455
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
456
|
+
|
|
457
|
+
// Even if the stream later finishes, no follow-up turn starts.
|
|
458
|
+
await act(async () => {
|
|
459
|
+
streams[0]!.finish();
|
|
460
|
+
});
|
|
461
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
462
|
+
expect(streams).toHaveLength(1);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
@@ -70,6 +70,11 @@ export interface UseChatStreamOptions {
|
|
|
70
70
|
export interface UseChatStreamReturn {
|
|
71
71
|
messages: ChatMessage[];
|
|
72
72
|
isStreaming: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Send a user message. Queue-safe (W6-2): if a turn is currently streaming
|
|
75
|
+
* the message is NOT dropped — it is appended to the follow-up queue and
|
|
76
|
+
* sent automatically as the next turn when the current one ends.
|
|
77
|
+
*/
|
|
73
78
|
sendUserMessage: (input: ChatStreamSendInput) => Promise<void>;
|
|
74
79
|
/**
|
|
75
80
|
* Steer the running turn with an extra user message. No-op resolving
|
|
@@ -78,6 +83,17 @@ export interface UseChatStreamReturn {
|
|
|
78
83
|
* consumes it at the next round boundary).
|
|
79
84
|
*/
|
|
80
85
|
steerUserMessage: (content: string) => Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Explicitly queue a message as a follow-up (W6-2): sent automatically as
|
|
88
|
+
* the next turn once the current turn finishes. When not streaming this is
|
|
89
|
+
* equivalent to `sendUserMessage`. Distinct from `steerUserMessage`, which
|
|
90
|
+
* redirects the *current* turn mid-flight.
|
|
91
|
+
*/
|
|
92
|
+
followUpUserMessage: (input: ChatStreamSendInput) => void;
|
|
93
|
+
/** Follow-up messages queued while streaming, in order (not yet sent). */
|
|
94
|
+
pendingFollowUps: ChatStreamSendInput[];
|
|
95
|
+
/** Remove a queued follow-up by index (e.g. the user withdraws it). */
|
|
96
|
+
removeFollowUp: (index: number) => void;
|
|
81
97
|
cancel: () => void;
|
|
82
98
|
/** Replace the message buffer (e.g. when switching chat). */
|
|
83
99
|
setMessages: (messages: ChatMessage[]) => void;
|
|
@@ -226,6 +242,14 @@ export function useChatStream(
|
|
|
226
242
|
const isStreamingRef = useRef(false);
|
|
227
243
|
const cancelRef = useRef<(() => void) | null>(null);
|
|
228
244
|
const [, forceRerender] = useReducer((x: number) => x + 1, 0);
|
|
245
|
+
// W6-2 follow-up queue: messages submitted while a turn is streaming. Kept
|
|
246
|
+
// in a ref (drained imperatively at turn end) with a version counter to
|
|
247
|
+
// re-render consumers that show the pending queue.
|
|
248
|
+
const followUpsRef = useRef<ChatStreamSendInput[]>([]);
|
|
249
|
+
const [followUpVersion, bumpFollowUpVersion] = useReducer((x: number) => x + 1, 0);
|
|
250
|
+
// Self-handle so a finishing turn can start the next queued turn without a
|
|
251
|
+
// circular useCallback dependency.
|
|
252
|
+
const sendRef = useRef<(input: ChatStreamSendInput) => Promise<void>>();
|
|
229
253
|
|
|
230
254
|
const setStreaming = useCallback((next: boolean) => {
|
|
231
255
|
isStreamingRef.current = next;
|
|
@@ -292,6 +316,10 @@ export function useChatStream(
|
|
|
292
316
|
const sendUserMessage = useCallback(
|
|
293
317
|
async (input: ChatStreamSendInput) => {
|
|
294
318
|
if (isStreamingRef.current) {
|
|
319
|
+
// W6-2: queue instead of dropping. The finishing turn's `finally`
|
|
320
|
+
// drains the queue and starts the next turn automatically.
|
|
321
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
322
|
+
bumpFollowUpVersion();
|
|
295
323
|
return;
|
|
296
324
|
}
|
|
297
325
|
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
@@ -312,12 +340,42 @@ export function useChatStream(
|
|
|
312
340
|
} finally {
|
|
313
341
|
cancelRef.current = null;
|
|
314
342
|
setStreaming(false);
|
|
343
|
+
// W6-2: drain the follow-up queue — send the next queued message as
|
|
344
|
+
// a fresh turn. isStreamingRef is already false here, so the nested
|
|
345
|
+
// call starts a new turn rather than re-queueing.
|
|
346
|
+
const next = followUpsRef.current.shift();
|
|
347
|
+
if (next !== undefined) {
|
|
348
|
+
bumpFollowUpVersion();
|
|
349
|
+
void sendRef.current?.(next);
|
|
350
|
+
}
|
|
315
351
|
}
|
|
316
352
|
},
|
|
317
353
|
[handleEvent, options.transport, setStreaming],
|
|
318
354
|
);
|
|
319
355
|
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
sendRef.current = sendUserMessage;
|
|
358
|
+
}, [sendUserMessage]);
|
|
359
|
+
|
|
360
|
+
const followUpUserMessage = useCallback((input: ChatStreamSendInput) => {
|
|
361
|
+
if (!isStreamingRef.current) {
|
|
362
|
+
void sendRef.current?.(input);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
366
|
+
bumpFollowUpVersion();
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
const removeFollowUp = useCallback((index: number) => {
|
|
370
|
+
followUpsRef.current = followUpsRef.current.filter((_, i) => i !== index);
|
|
371
|
+
bumpFollowUpVersion();
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
320
374
|
const cancel = useCallback(() => {
|
|
375
|
+
// Cancelling the current turn also drops queued follow-ups — an explicit
|
|
376
|
+
// stop means "don't keep going", so we don't auto-send what was queued.
|
|
377
|
+
followUpsRef.current = [];
|
|
378
|
+
bumpFollowUpVersion();
|
|
321
379
|
if (cancelRef.current) {
|
|
322
380
|
cancelRef.current();
|
|
323
381
|
cancelRef.current = null;
|
|
@@ -363,10 +421,25 @@ export function useChatStream(
|
|
|
363
421
|
isStreaming: isStreamingRef.current,
|
|
364
422
|
sendUserMessage,
|
|
365
423
|
steerUserMessage,
|
|
424
|
+
followUpUserMessage,
|
|
425
|
+
// followUpVersion re-keys this memo so queue changes surface to the UI.
|
|
426
|
+
pendingFollowUps: followUpsRef.current,
|
|
427
|
+
removeFollowUp,
|
|
366
428
|
cancel,
|
|
367
429
|
setMessages,
|
|
368
430
|
appendMessage,
|
|
369
431
|
}),
|
|
370
|
-
|
|
432
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
433
|
+
[
|
|
434
|
+
state.messages,
|
|
435
|
+
sendUserMessage,
|
|
436
|
+
steerUserMessage,
|
|
437
|
+
followUpUserMessage,
|
|
438
|
+
removeFollowUp,
|
|
439
|
+
cancel,
|
|
440
|
+
setMessages,
|
|
441
|
+
appendMessage,
|
|
442
|
+
followUpVersion,
|
|
443
|
+
],
|
|
371
444
|
);
|
|
372
445
|
}
|