@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.
@@ -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,GACpB,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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAO3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,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,CAsJrB"}
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
- const sendUserMessage = useCallback(async (input) => {
197
- if (isStreamingRef.current) {
198
- return;
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
- }), [state.messages, sendUserMessage, steerUserMessage, cancel, setMessages, appendMessage]);
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":"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"}
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.2.5",
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.2.5"
60
+ "@steerable/agent-protocol": "0.4.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=18.0.0",
@@ -4,6 +4,7 @@ export type {
4
4
  UseChatStreamReturn,
5
5
  ChatStreamTransport,
6
6
  ChatStreamSendInput,
7
+ SteerOutcome,
7
8
  } from './useChatStream.js';
8
9
 
9
10
  export { useToolCallStatus } from './useToolCallStatus.js';
@@ -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` is a no-op while `isStreaming` is true. Wire your composer's send button to `disabled={isStreaming}` to mirror this in the UI.
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
- const sendUserMessage = useCallback(
293
- async (input: ChatStreamSendInput) => {
294
- if (isStreamingRef.current) {
295
- return;
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
- [state.messages, sendUserMessage, steerUserMessage, cancel, setMessages, appendMessage],
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
  }