@steerable/agent-ui 0.6.51 → 0.6.53

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.
@@ -94,8 +94,10 @@ export interface UseChatStreamReturn {
94
94
  /**
95
95
  * Steer the running turn with an extra user message. No-op resolving
96
96
  * `false` when not streaming or the transport has no `steer`. On success
97
- * the message is appended to the visible transcript immediately (the loop
98
- * consumes it at the next round boundary).
97
+ * the message is inserted immediately before the in-flight assistant
98
+ * message, matching the persisted order (hosts store the steer at
99
+ * acceptance, the reply at turn end), so the streaming assistant stays the
100
+ * list tail. The loop consumes it at the next round boundary.
99
101
  */
100
102
  steerUserMessage: (content: string) => Promise<boolean>;
101
103
  /**
@@ -15,6 +15,14 @@ function reducer(state, action) {
15
15
  return { messages: action.messages };
16
16
  case 'append':
17
17
  return { messages: [...state.messages, action.message] };
18
+ case 'insert-before-last-assistant': {
19
+ const idx = findLastAssistantIndex(state.messages);
20
+ if (idx === -1)
21
+ return { messages: [...state.messages, action.message] };
22
+ const next = state.messages.slice();
23
+ next.splice(idx, 0, action.message);
24
+ return { messages: next };
25
+ }
18
26
  case 'patch-last-assistant': {
19
27
  const idx = findLastAssistantIndex(state.messages);
20
28
  if (idx === -1)
@@ -286,7 +294,7 @@ export function useChatStream(options) {
286
294
  }
287
295
  const ok = await steer(content);
288
296
  if (ok) {
289
- dispatch({ type: 'append', message: newUserMessage(content) });
297
+ dispatch({ type: 'insert-before-last-assistant', message: newUserMessage(content) });
290
298
  }
291
299
  return ok;
292
300
  }, [options.transport]);
@@ -303,7 +311,7 @@ export function useChatStream(options) {
303
311
  if (steer) {
304
312
  const ok = await steer(content);
305
313
  if (ok) {
306
- dispatch({ type: 'append', message: newUserMessage(content) });
314
+ dispatch({ type: 'insert-before-last-assistant', message: newUserMessage(content) });
307
315
  return 'steered';
308
316
  }
309
317
  // The turn may have ended while the steer was in flight; its
@@ -19,10 +19,9 @@ export interface UseToolCallStatusOptions {
19
19
  call: ToolCall;
20
20
  result?: ToolResult;
21
21
  /**
22
- * Optional override; when omitted the hook infers the mode from the tool
23
- * name pattern (`get_*` / `list_*` → read, `delete_*` / `archive_*` →
24
- * destructive, `local_*` → local, etc.). Mirrors the framework's harness
25
- * `decide_tool_mode`.
22
+ * Mode declared for this tool. First-party tools set this from registration.
23
+ * When omitted, the hook falls back to a name-prefix table. That fallback
24
+ * is only for undeclared third-party names.
26
25
  */
27
26
  mode?: ToolCallMode;
28
27
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-ui",
3
- "version": "0.6.51",
3
+ "version": "0.6.53",
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.6.51"
60
+ "@steerable/agent-protocol": "0.6.53"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=18.0.0",
@@ -384,9 +384,13 @@ describe('steerOrFollowUpUserMessage', () => {
384
384
  outcome = await result.current.steerOrFollowUpUserMessage('补充一句');
385
385
  });
386
386
  expect(outcome).toBe('steered');
387
- expect(
388
- result.current.messages.some((m) => m.role === 'user' && m.content === '补充一句'),
389
- ).toBe(true);
387
+ // Inserted before the in-flight assistant (matches the persisted order),
388
+ // so the streaming assistant stays the list tail.
389
+ expect(result.current.messages.map((m) => [m.role, m.content])).toEqual([
390
+ ['user', 'start'],
391
+ ['user', '补充一句'],
392
+ ['assistant', ''],
393
+ ]);
390
394
  expect(result.current.pendingFollowUps).toEqual([]);
391
395
  expect(streams).toHaveLength(1);
392
396
 
@@ -107,8 +107,10 @@ export interface UseChatStreamReturn {
107
107
  /**
108
108
  * Steer the running turn with an extra user message. No-op resolving
109
109
  * `false` when not streaming or the transport has no `steer`. On success
110
- * the message is appended to the visible transcript immediately (the loop
111
- * consumes it at the next round boundary).
110
+ * the message is inserted immediately before the in-flight assistant
111
+ * message, matching the persisted order (hosts store the steer at
112
+ * acceptance, the reply at turn end), so the streaming assistant stays the
113
+ * list tail. The loop consumes it at the next round boundary.
112
114
  */
113
115
  steerUserMessage: (content: string) => Promise<boolean>;
114
116
  /**
@@ -142,6 +144,7 @@ export interface UseChatStreamReturn {
142
144
  type Action =
143
145
  | { type: 'reset'; messages: ChatMessage[] }
144
146
  | { type: 'append'; message: ChatMessage }
147
+ | { type: 'insert-before-last-assistant'; message: ChatMessage }
145
148
  | { type: 'patch-last-assistant'; patch: Partial<ChatMessage> }
146
149
  | { type: 'append-content'; delta: string }
147
150
  | { type: 'append-tool-call'; call: ToolCall }
@@ -158,6 +161,13 @@ function reducer(state: State, action: Action): State {
158
161
  return { messages: action.messages };
159
162
  case 'append':
160
163
  return { messages: [...state.messages, action.message] };
164
+ case 'insert-before-last-assistant': {
165
+ const idx = findLastAssistantIndex(state.messages);
166
+ if (idx === -1) return { messages: [...state.messages, action.message] };
167
+ const next = state.messages.slice();
168
+ next.splice(idx, 0, action.message);
169
+ return { messages: next };
170
+ }
161
171
  case 'patch-last-assistant': {
162
172
  const idx = findLastAssistantIndex(state.messages);
163
173
  if (idx === -1) return state;
@@ -453,7 +463,7 @@ export function useChatStream(
453
463
  }
454
464
  const ok = await steer(content);
455
465
  if (ok) {
456
- dispatch({ type: 'append', message: newUserMessage(content) });
466
+ dispatch({ type: 'insert-before-last-assistant', message: newUserMessage(content) });
457
467
  }
458
468
  return ok;
459
469
  },
@@ -474,7 +484,7 @@ export function useChatStream(
474
484
  if (steer) {
475
485
  const ok = await steer(content);
476
486
  if (ok) {
477
- dispatch({ type: 'append', message: newUserMessage(content) });
487
+ dispatch({ type: 'insert-before-last-assistant', message: newUserMessage(content) });
478
488
  return 'steered';
479
489
  }
480
490
  // The turn may have ended while the steer was in flight; its
@@ -32,10 +32,9 @@ export interface UseToolCallStatusOptions {
32
32
  call: ToolCall;
33
33
  result?: ToolResult;
34
34
  /**
35
- * Optional override; when omitted the hook infers the mode from the tool
36
- * name pattern (`get_*` / `list_*` → read, `delete_*` / `archive_*` →
37
- * destructive, `local_*` → local, etc.). Mirrors the framework's harness
38
- * `decide_tool_mode`.
35
+ * Mode declared for this tool. First-party tools set this from registration.
36
+ * When omitted, the hook falls back to a name-prefix table. That fallback
37
+ * is only for undeclared third-party names.
39
38
  */
40
39
  mode?: ToolCallMode;
41
40
  /**