@steerable/agent-ui 0.2.5 → 0.4.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.
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useChatStream.d.ts +48 -0
- package/dist/hooks/useChatStream.d.ts.map +1 -1
- package/dist/hooks/useChatStream.js +110 -5
- package/dist/hooks/useChatStream.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useChatStream.mdx +23 -1
- package/src/hooks/useChatStream.test.ts +393 -0
- package/src/hooks/useChatStream.ts +174 -6
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useChatStream } from './useChatStream.js';
|
|
2
|
-
export type { UseChatStreamOptions, UseChatStreamReturn, ChatStreamTransport, ChatStreamSendInput, } from './useChatStream.js';
|
|
2
|
+
export type { UseChatStreamOptions, UseChatStreamReturn, ChatStreamTransport, ChatStreamSendInput, SteerOutcome, } from './useChatStream.js';
|
|
3
3
|
export { useToolCallStatus } from './useToolCallStatus.js';
|
|
4
4
|
export type { ToolCallStatus, UseToolCallStatusOptions, UseToolCallStatusReturn, } from './useToolCallStatus.js';
|
|
5
5
|
export { useAgentSession } from './useAgentSession.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EACV,cAAc,EACd,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC"}
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AASnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAO3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -35,6 +35,17 @@ export interface ChatStreamTransport {
|
|
|
35
35
|
*/
|
|
36
36
|
steer?: (content: string) => Promise<boolean>;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Where `steerOrFollowUpUserMessage` actually delivered the message:
|
|
40
|
+
* - `'steered'` — the running turn accepted the injection.
|
|
41
|
+
* - `'queued'` — the turn is still running but not steerable; the message
|
|
42
|
+
* sits in the W6-2 follow-up queue and auto-sends at turn end.
|
|
43
|
+
* - `'sent'` — no turn was running by the time the steer attempt settled;
|
|
44
|
+
* the message went out immediately as a normal new turn.
|
|
45
|
+
* The union lets callers render their own locale-specific notice per
|
|
46
|
+
* outcome; the hook itself carries no user-facing copy.
|
|
47
|
+
*/
|
|
48
|
+
export type SteerOutcome = 'steered' | 'queued' | 'sent';
|
|
38
49
|
export interface UseChatStreamOptions {
|
|
39
50
|
/** Required transport handle; see `ChatStreamTransport`. */
|
|
40
51
|
transport: ChatStreamTransport;
|
|
@@ -56,7 +67,23 @@ export interface UseChatStreamOptions {
|
|
|
56
67
|
export interface UseChatStreamReturn {
|
|
57
68
|
messages: ChatMessage[];
|
|
58
69
|
isStreaming: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Send a user message. Queue-safe (W6-2): if a turn is currently streaming
|
|
72
|
+
* the message is NOT dropped — it is appended to the follow-up queue and
|
|
73
|
+
* sent automatically as the next turn when the current one ends.
|
|
74
|
+
*/
|
|
59
75
|
sendUserMessage: (input: ChatStreamSendInput) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* W7-1: resume the interrupted turn recorded on the backend. Streams an
|
|
78
|
+
* assistant turn WITHOUT appending a user message — the transport receives
|
|
79
|
+
* `{ content: '', metadata: { ...input?.metadata, resume: true } }` and the
|
|
80
|
+
* backend replays the durable record's projection as the loop seed, so the
|
|
81
|
+
* user never retypes the interrupted request. No-op while streaming (a
|
|
82
|
+
* resume is not queueable as a follow-up — it is not a user message).
|
|
83
|
+
*/
|
|
84
|
+
resumeTurn: (input?: {
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
86
|
+
}) => Promise<void>;
|
|
60
87
|
/**
|
|
61
88
|
* Steer the running turn with an extra user message. No-op resolving
|
|
62
89
|
* `false` when not streaming or the transport has no `steer`. On success
|
|
@@ -64,6 +91,27 @@ export interface UseChatStreamReturn {
|
|
|
64
91
|
* consumes it at the next round boundary).
|
|
65
92
|
*/
|
|
66
93
|
steerUserMessage: (content: string) => Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Steer with a never-drop fallback (W6-2): tries `steerUserMessage` first,
|
|
96
|
+
* then degrades based on live turn state — see `SteerOutcome` for the
|
|
97
|
+
* three outcomes. The turn-already-ended race is resolved by re-checking
|
|
98
|
+
* `isStreaming` after the steer attempt settles: a finished turn's
|
|
99
|
+
* `finally` has already drained the queue, so queueing there would strand
|
|
100
|
+
* the message until some future turn ends — it is sent directly instead.
|
|
101
|
+
* Prefer this over `steerUserMessage` for composer Enter handlers.
|
|
102
|
+
*/
|
|
103
|
+
steerOrFollowUpUserMessage: (content: string) => Promise<SteerOutcome>;
|
|
104
|
+
/**
|
|
105
|
+
* Explicitly queue a message as a follow-up (W6-2): sent automatically as
|
|
106
|
+
* the next turn once the current turn finishes. When not streaming this is
|
|
107
|
+
* equivalent to `sendUserMessage`. Distinct from `steerUserMessage`, which
|
|
108
|
+
* redirects the *current* turn mid-flight.
|
|
109
|
+
*/
|
|
110
|
+
followUpUserMessage: (input: ChatStreamSendInput) => void;
|
|
111
|
+
/** Follow-up messages queued while streaming, in order (not yet sent). */
|
|
112
|
+
pendingFollowUps: ChatStreamSendInput[];
|
|
113
|
+
/** Remove a queued follow-up by index (e.g. the user withdraws it). */
|
|
114
|
+
removeFollowUp: (index: number) => void;
|
|
67
115
|
cancel: () => void;
|
|
68
116
|
/** Replace the message buffer (e.g. when switching chat). */
|
|
69
117
|
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;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,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;;;;;;;OAOG;IACH,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD;;;;;;;;OAQG;IACH,0BAA0B,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACvE;;;;;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+QrB"}
|
|
@@ -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();
|
|
@@ -193,11 +201,14 @@ export function useChatStream(options) {
|
|
|
193
201
|
options.onUnknownEvent?.(event);
|
|
194
202
|
}
|
|
195
203
|
}, [options]);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
// Shared turn core: stream one assistant turn and drain the follow-up
|
|
205
|
+
// queue at the end. `echoUserMessage` is false only for W7-1 resume —
|
|
206
|
+
// the interrupted turn's user message is already in the transcript and
|
|
207
|
+
// the backend replays it from the durable record.
|
|
208
|
+
const startTurn = useCallback(async (input, opts) => {
|
|
209
|
+
if (opts.echoUserMessage) {
|
|
210
|
+
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
199
211
|
}
|
|
200
|
-
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
201
212
|
dispatch({ type: 'append', message: newAssistantPlaceholder() });
|
|
202
213
|
setStreaming(true);
|
|
203
214
|
try {
|
|
@@ -216,9 +227,51 @@ export function useChatStream(options) {
|
|
|
216
227
|
finally {
|
|
217
228
|
cancelRef.current = null;
|
|
218
229
|
setStreaming(false);
|
|
230
|
+
// W6-2: drain the follow-up queue — send the next queued message as
|
|
231
|
+
// a fresh turn. isStreamingRef is already false here, so the nested
|
|
232
|
+
// call starts a new turn rather than re-queueing.
|
|
233
|
+
const next = followUpsRef.current.shift();
|
|
234
|
+
if (next !== undefined) {
|
|
235
|
+
bumpFollowUpVersion();
|
|
236
|
+
void sendRef.current?.(next);
|
|
237
|
+
}
|
|
219
238
|
}
|
|
220
239
|
}, [handleEvent, options.transport, setStreaming]);
|
|
240
|
+
const sendUserMessage = useCallback(async (input) => {
|
|
241
|
+
if (isStreamingRef.current) {
|
|
242
|
+
// W6-2: queue instead of dropping. The finishing turn's `finally`
|
|
243
|
+
// drains the queue and starts the next turn automatically.
|
|
244
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
245
|
+
bumpFollowUpVersion();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
await startTurn(input, { echoUserMessage: true });
|
|
249
|
+
}, [startTurn]);
|
|
250
|
+
const resumeTurn = useCallback(async (input) => {
|
|
251
|
+
if (isStreamingRef.current)
|
|
252
|
+
return;
|
|
253
|
+
await startTurn({ content: '', metadata: { ...input?.metadata, resume: true } }, { echoUserMessage: false });
|
|
254
|
+
}, [startTurn]);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
sendRef.current = sendUserMessage;
|
|
257
|
+
}, [sendUserMessage]);
|
|
258
|
+
const followUpUserMessage = useCallback((input) => {
|
|
259
|
+
if (!isStreamingRef.current) {
|
|
260
|
+
void sendRef.current?.(input);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
264
|
+
bumpFollowUpVersion();
|
|
265
|
+
}, []);
|
|
266
|
+
const removeFollowUp = useCallback((index) => {
|
|
267
|
+
followUpsRef.current = followUpsRef.current.filter((_, i) => i !== index);
|
|
268
|
+
bumpFollowUpVersion();
|
|
269
|
+
}, []);
|
|
221
270
|
const cancel = useCallback(() => {
|
|
271
|
+
// Cancelling the current turn also drops queued follow-ups — an explicit
|
|
272
|
+
// stop means "don't keep going", so we don't auto-send what was queued.
|
|
273
|
+
followUpsRef.current = [];
|
|
274
|
+
bumpFollowUpVersion();
|
|
222
275
|
if (cancelRef.current) {
|
|
223
276
|
cancelRef.current();
|
|
224
277
|
cancelRef.current = null;
|
|
@@ -236,6 +289,38 @@ export function useChatStream(options) {
|
|
|
236
289
|
}
|
|
237
290
|
return ok;
|
|
238
291
|
}, [options.transport]);
|
|
292
|
+
const steerOrFollowUpUserMessage = useCallback(async (content) => {
|
|
293
|
+
const input = { content };
|
|
294
|
+
// No turn running: there is nothing to inject into and nothing whose
|
|
295
|
+
// `finally` would drain the queue — a normal send is the only route
|
|
296
|
+
// that delivers the message.
|
|
297
|
+
if (!isStreamingRef.current) {
|
|
298
|
+
void sendRef.current?.(input);
|
|
299
|
+
return 'sent';
|
|
300
|
+
}
|
|
301
|
+
const steer = options.transport.steer;
|
|
302
|
+
if (steer) {
|
|
303
|
+
const ok = await steer(content);
|
|
304
|
+
if (ok) {
|
|
305
|
+
dispatch({ type: 'append', message: newUserMessage(content) });
|
|
306
|
+
return 'steered';
|
|
307
|
+
}
|
|
308
|
+
// The turn may have ended while the steer was in flight; its
|
|
309
|
+
// `finally` already ran the queue drain, so a queued message would
|
|
310
|
+
// sit unsent until an unrelated future turn ends. Send it as a fresh
|
|
311
|
+
// turn instead. If a queued follow-up already started the next turn,
|
|
312
|
+
// isStreamingRef is true again and queueing below is correct.
|
|
313
|
+
if (!isStreamingRef.current) {
|
|
314
|
+
void sendRef.current?.(input);
|
|
315
|
+
return 'sent';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Turn still running but not steerable (transport lacks `steer`, or
|
|
319
|
+
// the loop rejected the injection): W6-2 queue, drained at turn end.
|
|
320
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
321
|
+
bumpFollowUpVersion();
|
|
322
|
+
return 'queued';
|
|
323
|
+
}, [options.transport]);
|
|
239
324
|
const setMessages = useCallback((messages) => {
|
|
240
325
|
dispatch({ type: 'reset', messages });
|
|
241
326
|
}, []);
|
|
@@ -254,10 +339,30 @@ export function useChatStream(options) {
|
|
|
254
339
|
messages: state.messages,
|
|
255
340
|
isStreaming: isStreamingRef.current,
|
|
256
341
|
sendUserMessage,
|
|
342
|
+
resumeTurn,
|
|
343
|
+
steerUserMessage,
|
|
344
|
+
steerOrFollowUpUserMessage,
|
|
345
|
+
followUpUserMessage,
|
|
346
|
+
// followUpVersion re-keys this memo so queue changes surface to the UI.
|
|
347
|
+
pendingFollowUps: followUpsRef.current,
|
|
348
|
+
removeFollowUp,
|
|
349
|
+
cancel,
|
|
350
|
+
setMessages,
|
|
351
|
+
appendMessage,
|
|
352
|
+
}),
|
|
353
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
354
|
+
[
|
|
355
|
+
state.messages,
|
|
356
|
+
sendUserMessage,
|
|
357
|
+
resumeTurn,
|
|
257
358
|
steerUserMessage,
|
|
359
|
+
steerOrFollowUpUserMessage,
|
|
360
|
+
followUpUserMessage,
|
|
361
|
+
removeFollowUp,
|
|
258
362
|
cancel,
|
|
259
363
|
setMessages,
|
|
260
364
|
appendMessage,
|
|
261
|
-
|
|
365
|
+
followUpVersion,
|
|
366
|
+
]);
|
|
262
367
|
}
|
|
263
368
|
//# 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;AAyF5E,SAAS,OAAO,CAAC,KAAY,EAAE,MAAc;IAC3C,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,OAAO;YACV,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACvC,KAAK,QAAQ;YACX,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3D,KAAK,sBAAsB,CAAC,CAAC,CAAC;YAC5B,MAAM,GAAG,GAAG,sBAAsB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnD,IAAI,GAAG,KAAK,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7B,MAAM,OAAO,GAAgB;gBAC3B,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;gBACtB,GAAG,MAAM,CAAC,KAAK;aACD,CAAC;YACjB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;YACpB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC5B,CAAC;QACD,KAAK,gBAAgB,CAAC,CAAC,CAAC;YACtB,MAAM,GAAG,GAAG,sBAAsB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnD,IAAI,GAAG,KAAK,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,OAAO,GAAgB;gBAC3B,GAAG,MAAM;gBACT,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK;aAChC,CAAC;YACjB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;YACpB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC5B,CAAC;QACD,KAAK,kBAAkB,CAAC,CAAC,CAAC;YACxB,MAAM,GAAG,GAAG,sBAAsB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnD,IAAI,GAAG,KAAK,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACxB,MAAM,OAAO,GAAgB,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,KAAK,EAAiB,CAAC;YAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;YACpB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC5B,CAAC;QACD,KAAK,oBAAoB,CAAC,CAAC,CAAC;YAC1B,wEAAwE;YACxE,qEAAqE;YACrE,MAAM,GAAG,GAAG,sBAAsB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnD,IAAI,GAAG,KAAK,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,OAAO,GAAgB;gBAC3B,GAAG,MAAM;gBACT,UAAU,EAAE,MAAM,CAAC,MAAM;aACX,CAAC;YACjB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;YACpB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC5B,CAAC;QACD,KAAK,oBAAoB;YACvB,OAAO,KAAK,CAAC;QACf;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAuB;IACrD,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,CAAC,CAAC,CAAC;AACZ,CAAC;AAED,SAAS,uBAAuB;IAC9B,OAAO;QACL,EAAE,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE;QAClE,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,EAAE;QACX,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,OAAe;IACrC,OAAO;QACL,EAAE,EAAE,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE;QAC3D,IAAI,EAAE,MAAM;QACZ,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAe;IACvC,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClE,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,2EAA2E;IAC3E,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAA4B,CAAC;IACjE,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC;IAC5D,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC;IAChE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,KAAe;IACnC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAkB,CAAC;IACzC,IACE,OAAO;QACP,OAAO,OAAO,KAAK,QAAQ;QAC3B,IAAI,IAAI,OAAO;QACf,MAAM,IAAI,OAAO;QACjB,WAAW,IAAI,OAAO,EACtB,CAAC;QACD,OAAO,OAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,cAAc,CAAC,KAAe;IACrC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAkB,CAAC;IACzC,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;QACnE,OAAO,OAAqB,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,OAA6B;IAE7B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE;QAC5C,QAAQ,EAAE,OAAO,CAAC,eAAe,IAAI,EAAE;KACxC,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,SAAS,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,aAAa,CAAC,GAAG,UAAU,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9D,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,IAAa,EAAE,EAAE;QACjD,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;QAC9B,aAAa,EAAE,CAAC;IAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,KAAe,EAAE,EAAE;QAClB,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;gBACtC,IAAI,KAAK;oBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAC;gBACvD,OAAO;YACT,CAAC;YACD,KAAK,WAAW,CAAC,CAAC,CAAC;gBACjB,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;gBACjC,IAAI,IAAI;oBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvD,OAAO;YACT,CAAC;YACD,KAAK,aAAa,CAAC,CAAC,CAAC;gBACnB,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;gBACrC,IAAI,CAAC,MAAM;oBAAE,OAAO;gBACpB,MAAM,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,MAAM,CAAC,CAAC;gBACrD,IAAI,MAAM,EAAE,CAAC;oBACX,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;gBAChD,CAAC;qBAAM,CAAC;oBACN,QAAQ,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,CAAC,CAAC;gBACnD,CAAC;gBACD,OAAO;YACT,CAAC;YACD,KAAK,MAAM;gBACT,QAAQ,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC,CAAC;gBACzC,OAAO;YACT,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,cAAc,CAAC;gBAC5C,QAAQ,CAAC;oBACP,IAAI,EAAE,sBAAsB;oBAC5B,KAAK,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,EAAE,EAAE;iBAC5C,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,KAAK,kBAAkB,CAAC,CAAC,CAAC;gBACxB,QAAQ,CAAC;oBACP,IAAI,EAAE,sBAAsB;oBAC5B,KAAK,EAAE;wBACL,OAAO,EACL,KAAK,CAAC,OAAO;4BACb,kDAAkD;qBACrD;iBACF,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,KAAK,OAAO,CAAC;YACb,KAAK,eAAe,CAAC;YACrB,KAAK,aAAa,CAAC;YACnB,KAAK,WAAW,CAAC;YACjB;gBACE,OAAO,CAAC,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,MAAM,eAAe,GAAG,WAAW,CACjC,KAAK,EAAE,KAA0B,EAAE,EAAE;QACnC,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrE,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE,CAAC,CAAC;QACjE,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;YAClE,SAAS,CAAC,OAAO,GAAG,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACnE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC;gBACP,IAAI,EAAE,sBAAsB;gBAC5B,KAAK,EAAE;oBACL,OAAO,EACL,iBAAiB;wBACjB,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;iBACrD;aACF,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EACD,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAC/C,CAAC;IAEF,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,SAAS,CAAC,OAAO,EAAE,CAAC;YACpB,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,MAAM,gBAAgB,GAAG,WAAW,CAClC,KAAK,EAAE,OAAe,EAAoB,EAAE;QAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC;QACtC,IAAI,CAAC,cAAc,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YACzD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,EAAE,EAAE,CAAC;YACP,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC,EACD,CAAC,OAAO,CAAC,SAAS,CAAC,CACpB,CAAC;IAEF,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,QAAuB,EAAE,EAAE;QAC1D,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,OAAoB,EAAE,EAAE;QACzD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACxC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE;YACV,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;gBACtB,SAAS,CAAC,OAAO,EAAE,CAAC;gBACpB,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,WAAW,EAAE,cAAc,CAAC,OAAO;QACnC,eAAe;QACf,gBAAgB;QAChB,MAAM;QACN,WAAW;QACX,aAAa;KACd,CAAC,EACF,CAAC,KAAK,CAAC,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,CAAC,CACxF,CAAC;AACJ,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.4.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.4.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=18.0.0",
|
package/src/hooks/index.ts
CHANGED
|
@@ -33,6 +33,12 @@ const {
|
|
|
33
33
|
messages, // ChatMessage[]
|
|
34
34
|
isStreaming, // boolean
|
|
35
35
|
sendUserMessage, // ({ content }) => Promise<void>
|
|
36
|
+
resumeTurn, // ({ metadata? }?) => Promise<void> — W7-1 crash resume
|
|
37
|
+
steerUserMessage, // (content) => Promise<boolean> — raw mid-turn steer
|
|
38
|
+
steerOrFollowUpUserMessage, // (content) => Promise<SteerOutcome> — steer with never-drop fallback
|
|
39
|
+
followUpUserMessage, // ({ content }) => void — queue for after the turn (W6-2)
|
|
40
|
+
pendingFollowUps, // ChatStreamSendInput[] — queued, not yet sent
|
|
41
|
+
removeFollowUp, // (index) => void — withdraw a queued message
|
|
36
42
|
cancel, // () => void
|
|
37
43
|
setMessages, // (messages) => void
|
|
38
44
|
appendMessage, // (message) => void
|
|
@@ -113,6 +119,19 @@ const { ... } = useChatStream({
|
|
|
113
119
|
});
|
|
114
120
|
```
|
|
115
121
|
|
|
122
|
+
## Steering and follow-ups (W6-2)
|
|
123
|
+
|
|
124
|
+
While a turn is streaming there are three ways to submit more input, with different delivery guarantees:
|
|
125
|
+
|
|
126
|
+
* `steerUserMessage(content)` — raw mid-turn steer. Resolves `true` when the running turn accepted the message (it is appended to the transcript at once); `false` when no steerable turn is active (no transport `steer`, or the turn already ended). On `false` nothing else happens — what to do with the draft is the caller's problem.
|
|
127
|
+
* `followUpUserMessage(input)` — queue the message; it is sent automatically as the next turn when the current turn's `finally` drains the queue. `pendingFollowUps` exposes the queue (in order, not yet sent) and `removeFollowUp(index)` withdraws one. `sendUserMessage` while streaming degrades into this same queue rather than dropping.
|
|
128
|
+
* `steerOrFollowUpUserMessage(content)` — the never-drop combinator, intended for composer Enter handlers. It tries the steer first and resolves a `SteerOutcome` telling the caller where the message landed:
|
|
129
|
+
* `'steered'` — injected into the running turn.
|
|
130
|
+
* `'queued'` — the turn is still running but not steerable; the message sits in the follow-up queue and auto-sends at turn end.
|
|
131
|
+
* `'sent'` — no turn was running by the time the steer attempt settled. This covers the race where the turn ends while `transport.steer` is in flight: that turn's `finally` already drained the queue, so queueing would strand the message until some unrelated future turn ends — it is sent immediately as a normal new turn instead.
|
|
132
|
+
|
|
133
|
+
The hook exposes only the neutral outcome; rendering any notice ("queued instead of injected", "N queued will be discarded") is the caller's job, in the caller's locale.
|
|
134
|
+
|
|
116
135
|
## Cancellation
|
|
117
136
|
|
|
118
137
|
`transport.stream` may return a `() => void` cancel handle. The hook stores it and calls it when:
|
|
@@ -122,8 +141,11 @@ const { ... } = useChatStream({
|
|
|
122
141
|
|
|
123
142
|
If your transport can't cancel, return `void` — the hook will simply mark `isStreaming` as `false` when the promise settles.
|
|
124
143
|
|
|
144
|
+
`cancel()` also drops every queued follow-up (W6-2): an explicit stop means "don't keep going", so queued messages are not auto-sent afterwards. If your UI shows `pendingFollowUps`, tell the user that stopping discards them before they confirm.
|
|
145
|
+
|
|
125
146
|
## Caveats
|
|
126
147
|
|
|
127
|
-
* `sendUserMessage`
|
|
148
|
+
* `sendUserMessage` while `isStreaming` is true does NOT drop the message — it appends to the W6-2 follow-up queue and auto-sends as the next turn. If you'd rather block input during streaming, wire your composer's send button to `disabled={isStreaming}`.
|
|
149
|
+
* `resumeTurn` (W7-1) streams an assistant turn WITHOUT appending a user message — the transport receives `{ content: '', metadata: { resume: true } }` and the backend replays the durable record's projection as the loop seed. Use it for a "continue the interrupted turn" affordance after a crash/kill; it is a no-op while streaming and is never queued as a follow-up.
|
|
128
150
|
* Tool results are attached to the **latest assistant message**. If you need per-call lookup, override `renderToolCall` on `ChatPanel` and pull the result from your own map.
|
|
129
151
|
* Setting `toolResultToMessage` to a non-null returner switches to a "tool result lives in its own message" model — useful if you want to render tool results as standalone bubbles. Otherwise the result rides inline on the assistant turn.
|
|
@@ -12,9 +12,47 @@ 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,
|
|
17
|
+
type SteerOutcome,
|
|
16
18
|
} from './useChatStream';
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A transport whose streams stay open until the test explicitly finishes them,
|
|
22
|
+
* so we can submit follow-ups mid-stream and watch the queue drain.
|
|
23
|
+
*/
|
|
24
|
+
function makeControllableTransport(steer?: ChatStreamTransport['steer']) {
|
|
25
|
+
const streams: Array<{
|
|
26
|
+
input: ChatStreamSendInput;
|
|
27
|
+
onEvent: (e: SSEEvent) => void;
|
|
28
|
+
finish: () => void;
|
|
29
|
+
}> = [];
|
|
30
|
+
const transport: ChatStreamTransport = {
|
|
31
|
+
stream: (input, onEvent) =>
|
|
32
|
+
new Promise<void>((resolve) => {
|
|
33
|
+
streams.push({
|
|
34
|
+
input,
|
|
35
|
+
onEvent,
|
|
36
|
+
finish: () => {
|
|
37
|
+
onEvent({ type: 'done' });
|
|
38
|
+
resolve();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}),
|
|
42
|
+
...(steer ? { steer } : {}),
|
|
43
|
+
};
|
|
44
|
+
return { transport, streams };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Manually-resolved promise, for parking a steer attempt mid-flight. */
|
|
48
|
+
function makeDeferred<T>() {
|
|
49
|
+
let resolve!: (value: T) => void;
|
|
50
|
+
const promise = new Promise<T>((r) => {
|
|
51
|
+
resolve = r;
|
|
52
|
+
});
|
|
53
|
+
return { promise, resolve };
|
|
54
|
+
}
|
|
55
|
+
|
|
18
56
|
function makeTransport(
|
|
19
57
|
script: SSEEvent[][],
|
|
20
58
|
): { transport: ChatStreamTransport; cancel: ReturnType<typeof vi.fn> } {
|
|
@@ -309,3 +347,358 @@ describe('steerUserMessage', () => {
|
|
|
309
347
|
});
|
|
310
348
|
});
|
|
311
349
|
});
|
|
350
|
+
|
|
351
|
+
describe('steerOrFollowUpUserMessage', () => {
|
|
352
|
+
it("resolves 'steered' and appends the message when the turn accepts the injection", async () => {
|
|
353
|
+
const steer = vi.fn().mockResolvedValue(true);
|
|
354
|
+
const { transport, streams } = makeControllableTransport(steer);
|
|
355
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
356
|
+
|
|
357
|
+
act(() => {
|
|
358
|
+
void result.current.sendUserMessage({ content: 'start' });
|
|
359
|
+
});
|
|
360
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
361
|
+
|
|
362
|
+
let outcome: SteerOutcome | undefined;
|
|
363
|
+
await act(async () => {
|
|
364
|
+
outcome = await result.current.steerOrFollowUpUserMessage('补充一句');
|
|
365
|
+
});
|
|
366
|
+
expect(outcome).toBe('steered');
|
|
367
|
+
expect(
|
|
368
|
+
result.current.messages.some((m) => m.role === 'user' && m.content === '补充一句'),
|
|
369
|
+
).toBe(true);
|
|
370
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
371
|
+
expect(streams).toHaveLength(1);
|
|
372
|
+
|
|
373
|
+
await act(async () => {
|
|
374
|
+
streams[0]!.finish();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("resolves 'queued' when the transport has no steer, then drains at turn end", async () => {
|
|
379
|
+
const { transport, streams } = makeControllableTransport();
|
|
380
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
381
|
+
|
|
382
|
+
act(() => {
|
|
383
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
384
|
+
});
|
|
385
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
386
|
+
|
|
387
|
+
let outcome: SteerOutcome | undefined;
|
|
388
|
+
await act(async () => {
|
|
389
|
+
outcome = await result.current.steerOrFollowUpUserMessage('排队等下轮');
|
|
390
|
+
});
|
|
391
|
+
expect(outcome).toBe('queued');
|
|
392
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: '排队等下轮' }]);
|
|
393
|
+
|
|
394
|
+
await act(async () => {
|
|
395
|
+
streams[0]!.finish();
|
|
396
|
+
});
|
|
397
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
398
|
+
expect(streams[1]!.input.content).toBe('排队等下轮');
|
|
399
|
+
await act(async () => {
|
|
400
|
+
streams[1]!.finish();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("resolves 'queued' when the steer is rejected and the turn is still running", async () => {
|
|
405
|
+
const steer = vi.fn().mockResolvedValue(false);
|
|
406
|
+
const { transport, streams } = makeControllableTransport(steer);
|
|
407
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
408
|
+
|
|
409
|
+
act(() => {
|
|
410
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
411
|
+
});
|
|
412
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
413
|
+
|
|
414
|
+
let outcome: SteerOutcome | undefined;
|
|
415
|
+
await act(async () => {
|
|
416
|
+
outcome = await result.current.steerOrFollowUpUserMessage('拒收的转向');
|
|
417
|
+
});
|
|
418
|
+
expect(outcome).toBe('queued');
|
|
419
|
+
expect(steer).toHaveBeenCalledWith('拒收的转向');
|
|
420
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: '拒收的转向' }]);
|
|
421
|
+
|
|
422
|
+
await act(async () => {
|
|
423
|
+
streams[0]!.finish();
|
|
424
|
+
});
|
|
425
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
426
|
+
expect(streams[1]!.input.content).toBe('拒收的转向');
|
|
427
|
+
await act(async () => {
|
|
428
|
+
streams[1]!.finish();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("resolves 'sent' when the turn ended while the steer attempt was in flight", async () => {
|
|
433
|
+
const gate = makeDeferred<boolean>();
|
|
434
|
+
const steer = vi.fn(() => gate.promise);
|
|
435
|
+
const { transport, streams } = makeControllableTransport(steer);
|
|
436
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
437
|
+
|
|
438
|
+
act(() => {
|
|
439
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
440
|
+
});
|
|
441
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
442
|
+
|
|
443
|
+
let outcomePromise: Promise<SteerOutcome>;
|
|
444
|
+
await act(async () => {
|
|
445
|
+
outcomePromise = result.current.steerOrFollowUpUserMessage('来迟的补充');
|
|
446
|
+
});
|
|
447
|
+
expect(steer).toHaveBeenCalledWith('来迟的补充');
|
|
448
|
+
|
|
449
|
+
// The turn ends while the steer is still pending: its `finally` drains
|
|
450
|
+
// the (empty) queue and clears isStreaming before the steer settles.
|
|
451
|
+
await act(async () => {
|
|
452
|
+
streams[0]!.finish();
|
|
453
|
+
});
|
|
454
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
455
|
+
|
|
456
|
+
let outcome: SteerOutcome | undefined;
|
|
457
|
+
await act(async () => {
|
|
458
|
+
gate.resolve(false);
|
|
459
|
+
outcome = await outcomePromise;
|
|
460
|
+
});
|
|
461
|
+
// Queueing here would strand the message (the drain already ran), so it
|
|
462
|
+
// goes out immediately as a fresh turn instead.
|
|
463
|
+
expect(outcome).toBe('sent');
|
|
464
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
465
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
466
|
+
expect(streams[1]!.input.content).toBe('来迟的补充');
|
|
467
|
+
await act(async () => {
|
|
468
|
+
streams[1]!.finish();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("resolves 'sent' without touching steer when no turn is streaming", async () => {
|
|
473
|
+
const steer = vi.fn().mockResolvedValue(true);
|
|
474
|
+
const { transport, streams } = makeControllableTransport(steer);
|
|
475
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
476
|
+
|
|
477
|
+
let outcome: SteerOutcome | undefined;
|
|
478
|
+
await act(async () => {
|
|
479
|
+
outcome = await result.current.steerOrFollowUpUserMessage('直接发出');
|
|
480
|
+
});
|
|
481
|
+
expect(outcome).toBe('sent');
|
|
482
|
+
expect(steer).not.toHaveBeenCalled();
|
|
483
|
+
expect(streams).toHaveLength(1);
|
|
484
|
+
expect(streams[0]!.input.content).toBe('直接发出');
|
|
485
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
486
|
+
|
|
487
|
+
await act(async () => {
|
|
488
|
+
streams[0]!.finish();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('follow-up queue (W6-2)', () => {
|
|
494
|
+
it('sendUserMessage while streaming queues instead of dropping, then auto-sends next turn', async () => {
|
|
495
|
+
const { transport, streams } = makeControllableTransport();
|
|
496
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
497
|
+
|
|
498
|
+
// Turn 1 starts and stays open.
|
|
499
|
+
act(() => {
|
|
500
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
501
|
+
});
|
|
502
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
503
|
+
expect(result.current.isStreaming).toBe(true);
|
|
504
|
+
|
|
505
|
+
// Submit a second message mid-stream — must be queued, not dropped.
|
|
506
|
+
await act(async () => {
|
|
507
|
+
await result.current.sendUserMessage({ content: 'second' });
|
|
508
|
+
});
|
|
509
|
+
expect(streams).toHaveLength(1); // no new turn yet
|
|
510
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: 'second' }]);
|
|
511
|
+
|
|
512
|
+
// Finish turn 1 → the queued message auto-sends as turn 2.
|
|
513
|
+
await act(async () => {
|
|
514
|
+
streams[0]!.finish();
|
|
515
|
+
});
|
|
516
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
517
|
+
expect(streams[1]!.input.content).toBe('second');
|
|
518
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
519
|
+
|
|
520
|
+
await act(async () => {
|
|
521
|
+
streams[1]!.finish();
|
|
522
|
+
});
|
|
523
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
524
|
+
// Both user messages landed in the transcript.
|
|
525
|
+
const userTexts = result.current.messages
|
|
526
|
+
.filter((m) => m.role === 'user')
|
|
527
|
+
.map((m) => m.content);
|
|
528
|
+
expect(userTexts).toEqual(['first', 'second']);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('followUpUserMessage queues while streaming and sends immediately when idle', async () => {
|
|
532
|
+
const { transport, streams } = makeControllableTransport();
|
|
533
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
534
|
+
|
|
535
|
+
// Idle: followUpUserMessage behaves like a normal send.
|
|
536
|
+
await act(async () => {
|
|
537
|
+
result.current.followUpUserMessage({ content: 'idle' });
|
|
538
|
+
});
|
|
539
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
540
|
+
expect(streams[0]!.input.content).toBe('idle');
|
|
541
|
+
|
|
542
|
+
// Streaming: queues.
|
|
543
|
+
await act(async () => {
|
|
544
|
+
result.current.followUpUserMessage({ content: 'queued' });
|
|
545
|
+
});
|
|
546
|
+
expect(streams).toHaveLength(1);
|
|
547
|
+
expect(result.current.pendingFollowUps).toEqual([{ content: 'queued' }]);
|
|
548
|
+
|
|
549
|
+
await act(async () => {
|
|
550
|
+
streams[0]!.finish();
|
|
551
|
+
});
|
|
552
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
553
|
+
expect(streams[1]!.input.content).toBe('queued');
|
|
554
|
+
await act(async () => {
|
|
555
|
+
streams[1]!.finish();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('removeFollowUp withdraws a queued message before it sends', async () => {
|
|
560
|
+
const { transport, streams } = makeControllableTransport();
|
|
561
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
562
|
+
|
|
563
|
+
act(() => {
|
|
564
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
565
|
+
});
|
|
566
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
567
|
+
|
|
568
|
+
await act(async () => {
|
|
569
|
+
result.current.followUpUserMessage({ content: 'keep' });
|
|
570
|
+
result.current.followUpUserMessage({ content: 'drop' });
|
|
571
|
+
});
|
|
572
|
+
expect(result.current.pendingFollowUps.map((m) => m.content)).toEqual(['keep', 'drop']);
|
|
573
|
+
|
|
574
|
+
await act(async () => {
|
|
575
|
+
result.current.removeFollowUp(1); // remove 'drop'
|
|
576
|
+
});
|
|
577
|
+
expect(result.current.pendingFollowUps.map((m) => m.content)).toEqual(['keep']);
|
|
578
|
+
|
|
579
|
+
await act(async () => {
|
|
580
|
+
streams[0]!.finish();
|
|
581
|
+
});
|
|
582
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
583
|
+
expect(streams[1]!.input.content).toBe('keep');
|
|
584
|
+
await act(async () => {
|
|
585
|
+
streams[1]!.finish();
|
|
586
|
+
});
|
|
587
|
+
// 'drop' never sent.
|
|
588
|
+
expect(streams).toHaveLength(2);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('cancel() clears the queue so nothing auto-sends afterwards', async () => {
|
|
592
|
+
const { transport, streams } = makeControllableTransport();
|
|
593
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
594
|
+
|
|
595
|
+
act(() => {
|
|
596
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
597
|
+
});
|
|
598
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
599
|
+
|
|
600
|
+
await act(async () => {
|
|
601
|
+
result.current.followUpUserMessage({ content: 'queued' });
|
|
602
|
+
});
|
|
603
|
+
expect(result.current.pendingFollowUps).toHaveLength(1);
|
|
604
|
+
|
|
605
|
+
await act(async () => {
|
|
606
|
+
result.current.cancel();
|
|
607
|
+
});
|
|
608
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
609
|
+
|
|
610
|
+
// Even if the stream later finishes, no follow-up turn starts.
|
|
611
|
+
await act(async () => {
|
|
612
|
+
streams[0]!.finish();
|
|
613
|
+
});
|
|
614
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
615
|
+
expect(streams).toHaveLength(1);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe('resumeTurn (W7-1)', () => {
|
|
620
|
+
it('streams an assistant turn with resume:true and NO new user message', async () => {
|
|
621
|
+
const { transport, streams } = makeControllableTransport();
|
|
622
|
+
const { result } = renderHook(() =>
|
|
623
|
+
useChatStream({
|
|
624
|
+
transport,
|
|
625
|
+
initialMessages: [
|
|
626
|
+
{
|
|
627
|
+
id: 'u1',
|
|
628
|
+
role: 'user',
|
|
629
|
+
content: '被中断的请求',
|
|
630
|
+
createdAt: new Date().toISOString(),
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
}),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
act(() => {
|
|
637
|
+
void result.current.resumeTurn({ metadata: { mode: 'plan' } });
|
|
638
|
+
});
|
|
639
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
640
|
+
await act(async () => {
|
|
641
|
+
streams[0]!.finish();
|
|
642
|
+
});
|
|
643
|
+
await waitFor(() => expect(result.current.isStreaming).toBe(false));
|
|
644
|
+
|
|
645
|
+
// The wire input carries the resume marker and the caller's metadata,
|
|
646
|
+
// with empty content — the backend replays the record, so no user text
|
|
647
|
+
// is re-sent.
|
|
648
|
+
expect(streams).toHaveLength(1);
|
|
649
|
+
expect(streams[0]!.input).toEqual({
|
|
650
|
+
content: '',
|
|
651
|
+
metadata: { mode: 'plan', resume: true },
|
|
652
|
+
});
|
|
653
|
+
// The transcript gains ONLY the assistant placeholder — the interrupted
|
|
654
|
+
// turn's user message is already there (initialMessages), not duplicated.
|
|
655
|
+
expect(result.current.messages.map((m) => m.role)).toEqual([
|
|
656
|
+
'user',
|
|
657
|
+
'assistant',
|
|
658
|
+
]);
|
|
659
|
+
expect(result.current.isStreaming).toBe(false);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('is a no-op while streaming (a resume is not queueable)', async () => {
|
|
663
|
+
const { transport, streams } = makeControllableTransport();
|
|
664
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
665
|
+
|
|
666
|
+
act(() => {
|
|
667
|
+
void result.current.sendUserMessage({ content: 'first' });
|
|
668
|
+
});
|
|
669
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
670
|
+
|
|
671
|
+
await act(async () => {
|
|
672
|
+
await result.current.resumeTurn();
|
|
673
|
+
});
|
|
674
|
+
expect(streams).toHaveLength(1);
|
|
675
|
+
expect(result.current.pendingFollowUps).toEqual([]);
|
|
676
|
+
|
|
677
|
+
await act(async () => {
|
|
678
|
+
streams[0]!.finish();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('drains the follow-up queue after the resumed turn ends', async () => {
|
|
683
|
+
const { transport, streams } = makeControllableTransport();
|
|
684
|
+
const { result } = renderHook(() => useChatStream({ transport }));
|
|
685
|
+
|
|
686
|
+
act(() => {
|
|
687
|
+
void result.current.resumeTurn();
|
|
688
|
+
});
|
|
689
|
+
await waitFor(() => expect(streams).toHaveLength(1));
|
|
690
|
+
|
|
691
|
+
await act(async () => {
|
|
692
|
+
result.current.followUpUserMessage({ content: 'next question' });
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
await act(async () => {
|
|
696
|
+
streams[0]!.finish();
|
|
697
|
+
});
|
|
698
|
+
await waitFor(() => expect(streams).toHaveLength(2));
|
|
699
|
+
expect(streams[1]!.input.content).toBe('next question');
|
|
700
|
+
await act(async () => {
|
|
701
|
+
streams[1]!.finish();
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
});
|
|
@@ -48,6 +48,18 @@ export interface ChatStreamTransport {
|
|
|
48
48
|
steer?: (content: string) => Promise<boolean>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Where `steerOrFollowUpUserMessage` actually delivered the message:
|
|
53
|
+
* - `'steered'` — the running turn accepted the injection.
|
|
54
|
+
* - `'queued'` — the turn is still running but not steerable; the message
|
|
55
|
+
* sits in the W6-2 follow-up queue and auto-sends at turn end.
|
|
56
|
+
* - `'sent'` — no turn was running by the time the steer attempt settled;
|
|
57
|
+
* the message went out immediately as a normal new turn.
|
|
58
|
+
* The union lets callers render their own locale-specific notice per
|
|
59
|
+
* outcome; the hook itself carries no user-facing copy.
|
|
60
|
+
*/
|
|
61
|
+
export type SteerOutcome = 'steered' | 'queued' | 'sent';
|
|
62
|
+
|
|
51
63
|
export interface UseChatStreamOptions {
|
|
52
64
|
/** Required transport handle; see `ChatStreamTransport`. */
|
|
53
65
|
transport: ChatStreamTransport;
|
|
@@ -70,7 +82,21 @@ export interface UseChatStreamOptions {
|
|
|
70
82
|
export interface UseChatStreamReturn {
|
|
71
83
|
messages: ChatMessage[];
|
|
72
84
|
isStreaming: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Send a user message. Queue-safe (W6-2): if a turn is currently streaming
|
|
87
|
+
* the message is NOT dropped — it is appended to the follow-up queue and
|
|
88
|
+
* sent automatically as the next turn when the current one ends.
|
|
89
|
+
*/
|
|
73
90
|
sendUserMessage: (input: ChatStreamSendInput) => Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* W7-1: resume the interrupted turn recorded on the backend. Streams an
|
|
93
|
+
* assistant turn WITHOUT appending a user message — the transport receives
|
|
94
|
+
* `{ content: '', metadata: { ...input?.metadata, resume: true } }` and the
|
|
95
|
+
* backend replays the durable record's projection as the loop seed, so the
|
|
96
|
+
* user never retypes the interrupted request. No-op while streaming (a
|
|
97
|
+
* resume is not queueable as a follow-up — it is not a user message).
|
|
98
|
+
*/
|
|
99
|
+
resumeTurn: (input?: { metadata?: Record<string, unknown> }) => Promise<void>;
|
|
74
100
|
/**
|
|
75
101
|
* Steer the running turn with an extra user message. No-op resolving
|
|
76
102
|
* `false` when not streaming or the transport has no `steer`. On success
|
|
@@ -78,6 +104,27 @@ export interface UseChatStreamReturn {
|
|
|
78
104
|
* consumes it at the next round boundary).
|
|
79
105
|
*/
|
|
80
106
|
steerUserMessage: (content: string) => Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Steer with a never-drop fallback (W6-2): tries `steerUserMessage` first,
|
|
109
|
+
* then degrades based on live turn state — see `SteerOutcome` for the
|
|
110
|
+
* three outcomes. The turn-already-ended race is resolved by re-checking
|
|
111
|
+
* `isStreaming` after the steer attempt settles: a finished turn's
|
|
112
|
+
* `finally` has already drained the queue, so queueing there would strand
|
|
113
|
+
* the message until some future turn ends — it is sent directly instead.
|
|
114
|
+
* Prefer this over `steerUserMessage` for composer Enter handlers.
|
|
115
|
+
*/
|
|
116
|
+
steerOrFollowUpUserMessage: (content: string) => Promise<SteerOutcome>;
|
|
117
|
+
/**
|
|
118
|
+
* Explicitly queue a message as a follow-up (W6-2): sent automatically as
|
|
119
|
+
* the next turn once the current turn finishes. When not streaming this is
|
|
120
|
+
* equivalent to `sendUserMessage`. Distinct from `steerUserMessage`, which
|
|
121
|
+
* redirects the *current* turn mid-flight.
|
|
122
|
+
*/
|
|
123
|
+
followUpUserMessage: (input: ChatStreamSendInput) => void;
|
|
124
|
+
/** Follow-up messages queued while streaming, in order (not yet sent). */
|
|
125
|
+
pendingFollowUps: ChatStreamSendInput[];
|
|
126
|
+
/** Remove a queued follow-up by index (e.g. the user withdraws it). */
|
|
127
|
+
removeFollowUp: (index: number) => void;
|
|
81
128
|
cancel: () => void;
|
|
82
129
|
/** Replace the message buffer (e.g. when switching chat). */
|
|
83
130
|
setMessages: (messages: ChatMessage[]) => void;
|
|
@@ -226,6 +273,14 @@ export function useChatStream(
|
|
|
226
273
|
const isStreamingRef = useRef(false);
|
|
227
274
|
const cancelRef = useRef<(() => void) | null>(null);
|
|
228
275
|
const [, forceRerender] = useReducer((x: number) => x + 1, 0);
|
|
276
|
+
// W6-2 follow-up queue: messages submitted while a turn is streaming. Kept
|
|
277
|
+
// in a ref (drained imperatively at turn end) with a version counter to
|
|
278
|
+
// re-render consumers that show the pending queue.
|
|
279
|
+
const followUpsRef = useRef<ChatStreamSendInput[]>([]);
|
|
280
|
+
const [followUpVersion, bumpFollowUpVersion] = useReducer((x: number) => x + 1, 0);
|
|
281
|
+
// Self-handle so a finishing turn can start the next queued turn without a
|
|
282
|
+
// circular useCallback dependency.
|
|
283
|
+
const sendRef = useRef<(input: ChatStreamSendInput) => Promise<void>>();
|
|
229
284
|
|
|
230
285
|
const setStreaming = useCallback((next: boolean) => {
|
|
231
286
|
isStreamingRef.current = next;
|
|
@@ -289,12 +344,15 @@ export function useChatStream(
|
|
|
289
344
|
[options],
|
|
290
345
|
);
|
|
291
346
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
347
|
+
// Shared turn core: stream one assistant turn and drain the follow-up
|
|
348
|
+
// queue at the end. `echoUserMessage` is false only for W7-1 resume —
|
|
349
|
+
// the interrupted turn's user message is already in the transcript and
|
|
350
|
+
// the backend replays it from the durable record.
|
|
351
|
+
const startTurn = useCallback(
|
|
352
|
+
async (input: ChatStreamSendInput, opts: { echoUserMessage: boolean }) => {
|
|
353
|
+
if (opts.echoUserMessage) {
|
|
354
|
+
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
296
355
|
}
|
|
297
|
-
dispatch({ type: 'append', message: newUserMessage(input.content) });
|
|
298
356
|
dispatch({ type: 'append', message: newAssistantPlaceholder() });
|
|
299
357
|
setStreaming(true);
|
|
300
358
|
try {
|
|
@@ -312,12 +370,67 @@ export function useChatStream(
|
|
|
312
370
|
} finally {
|
|
313
371
|
cancelRef.current = null;
|
|
314
372
|
setStreaming(false);
|
|
373
|
+
// W6-2: drain the follow-up queue — send the next queued message as
|
|
374
|
+
// a fresh turn. isStreamingRef is already false here, so the nested
|
|
375
|
+
// call starts a new turn rather than re-queueing.
|
|
376
|
+
const next = followUpsRef.current.shift();
|
|
377
|
+
if (next !== undefined) {
|
|
378
|
+
bumpFollowUpVersion();
|
|
379
|
+
void sendRef.current?.(next);
|
|
380
|
+
}
|
|
315
381
|
}
|
|
316
382
|
},
|
|
317
383
|
[handleEvent, options.transport, setStreaming],
|
|
318
384
|
);
|
|
319
385
|
|
|
386
|
+
const sendUserMessage = useCallback(
|
|
387
|
+
async (input: ChatStreamSendInput) => {
|
|
388
|
+
if (isStreamingRef.current) {
|
|
389
|
+
// W6-2: queue instead of dropping. The finishing turn's `finally`
|
|
390
|
+
// drains the queue and starts the next turn automatically.
|
|
391
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
392
|
+
bumpFollowUpVersion();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await startTurn(input, { echoUserMessage: true });
|
|
396
|
+
},
|
|
397
|
+
[startTurn],
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const resumeTurn = useCallback(
|
|
401
|
+
async (input?: { metadata?: Record<string, unknown> }) => {
|
|
402
|
+
if (isStreamingRef.current) return;
|
|
403
|
+
await startTurn(
|
|
404
|
+
{ content: '', metadata: { ...input?.metadata, resume: true } },
|
|
405
|
+
{ echoUserMessage: false },
|
|
406
|
+
);
|
|
407
|
+
},
|
|
408
|
+
[startTurn],
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
sendRef.current = sendUserMessage;
|
|
413
|
+
}, [sendUserMessage]);
|
|
414
|
+
|
|
415
|
+
const followUpUserMessage = useCallback((input: ChatStreamSendInput) => {
|
|
416
|
+
if (!isStreamingRef.current) {
|
|
417
|
+
void sendRef.current?.(input);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
421
|
+
bumpFollowUpVersion();
|
|
422
|
+
}, []);
|
|
423
|
+
|
|
424
|
+
const removeFollowUp = useCallback((index: number) => {
|
|
425
|
+
followUpsRef.current = followUpsRef.current.filter((_, i) => i !== index);
|
|
426
|
+
bumpFollowUpVersion();
|
|
427
|
+
}, []);
|
|
428
|
+
|
|
320
429
|
const cancel = useCallback(() => {
|
|
430
|
+
// Cancelling the current turn also drops queued follow-ups — an explicit
|
|
431
|
+
// stop means "don't keep going", so we don't auto-send what was queued.
|
|
432
|
+
followUpsRef.current = [];
|
|
433
|
+
bumpFollowUpVersion();
|
|
321
434
|
if (cancelRef.current) {
|
|
322
435
|
cancelRef.current();
|
|
323
436
|
cancelRef.current = null;
|
|
@@ -340,6 +453,42 @@ export function useChatStream(
|
|
|
340
453
|
[options.transport],
|
|
341
454
|
);
|
|
342
455
|
|
|
456
|
+
const steerOrFollowUpUserMessage = useCallback(
|
|
457
|
+
async (content: string): Promise<SteerOutcome> => {
|
|
458
|
+
const input: ChatStreamSendInput = { content };
|
|
459
|
+
// No turn running: there is nothing to inject into and nothing whose
|
|
460
|
+
// `finally` would drain the queue — a normal send is the only route
|
|
461
|
+
// that delivers the message.
|
|
462
|
+
if (!isStreamingRef.current) {
|
|
463
|
+
void sendRef.current?.(input);
|
|
464
|
+
return 'sent';
|
|
465
|
+
}
|
|
466
|
+
const steer = options.transport.steer;
|
|
467
|
+
if (steer) {
|
|
468
|
+
const ok = await steer(content);
|
|
469
|
+
if (ok) {
|
|
470
|
+
dispatch({ type: 'append', message: newUserMessage(content) });
|
|
471
|
+
return 'steered';
|
|
472
|
+
}
|
|
473
|
+
// The turn may have ended while the steer was in flight; its
|
|
474
|
+
// `finally` already ran the queue drain, so a queued message would
|
|
475
|
+
// sit unsent until an unrelated future turn ends. Send it as a fresh
|
|
476
|
+
// turn instead. If a queued follow-up already started the next turn,
|
|
477
|
+
// isStreamingRef is true again and queueing below is correct.
|
|
478
|
+
if (!isStreamingRef.current) {
|
|
479
|
+
void sendRef.current?.(input);
|
|
480
|
+
return 'sent';
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Turn still running but not steerable (transport lacks `steer`, or
|
|
484
|
+
// the loop rejected the injection): W6-2 queue, drained at turn end.
|
|
485
|
+
followUpsRef.current = [...followUpsRef.current, input];
|
|
486
|
+
bumpFollowUpVersion();
|
|
487
|
+
return 'queued';
|
|
488
|
+
},
|
|
489
|
+
[options.transport],
|
|
490
|
+
);
|
|
491
|
+
|
|
343
492
|
const setMessages = useCallback((messages: ChatMessage[]) => {
|
|
344
493
|
dispatch({ type: 'reset', messages });
|
|
345
494
|
}, []);
|
|
@@ -362,11 +511,30 @@ export function useChatStream(
|
|
|
362
511
|
messages: state.messages,
|
|
363
512
|
isStreaming: isStreamingRef.current,
|
|
364
513
|
sendUserMessage,
|
|
514
|
+
resumeTurn,
|
|
365
515
|
steerUserMessage,
|
|
516
|
+
steerOrFollowUpUserMessage,
|
|
517
|
+
followUpUserMessage,
|
|
518
|
+
// followUpVersion re-keys this memo so queue changes surface to the UI.
|
|
519
|
+
pendingFollowUps: followUpsRef.current,
|
|
520
|
+
removeFollowUp,
|
|
366
521
|
cancel,
|
|
367
522
|
setMessages,
|
|
368
523
|
appendMessage,
|
|
369
524
|
}),
|
|
370
|
-
|
|
525
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
526
|
+
[
|
|
527
|
+
state.messages,
|
|
528
|
+
sendUserMessage,
|
|
529
|
+
resumeTurn,
|
|
530
|
+
steerUserMessage,
|
|
531
|
+
steerOrFollowUpUserMessage,
|
|
532
|
+
followUpUserMessage,
|
|
533
|
+
removeFollowUp,
|
|
534
|
+
cancel,
|
|
535
|
+
setMessages,
|
|
536
|
+
appendMessage,
|
|
537
|
+
followUpVersion,
|
|
538
|
+
],
|
|
371
539
|
);
|
|
372
540
|
}
|