@springbrand/chat-client 0.1.3-alpha.2 → 0.1.3-alpha.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/chat-client",
3
- "version": "0.1.3-alpha.2",
3
+ "version": "0.1.3-alpha.21",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -13,7 +13,7 @@
13
13
  ".": "./src/index.ts"
14
14
  },
15
15
  "peerDependencies": {
16
- "agents": "^0.19.0",
16
+ "agents": "^0.20.1",
17
17
  "ai": "^7.0.0",
18
18
  "react": "^19.0.0"
19
19
  },
@@ -25,7 +25,7 @@
25
25
  "@types/react-dom": "^19.2.3",
26
26
  "react-dom": "^19.2.7",
27
27
  "typescript": "^7.0.2",
28
- "@springbrand/agent-runtime": "0.1.3-alpha.3"
28
+ "@springbrand/agent-runtime": "0.2.0-alpha.28"
29
29
  },
30
30
  "scripts": {
31
31
  "typecheck": "tsc --noEmit"
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { useAgent } from "agents/react";
3
3
 
4
- interface InboxState<Session> {
4
+ export interface InboxState<Session> {
5
5
  chats: Session[];
6
6
  workspaceRev?: number;
7
7
  }
@@ -33,8 +33,11 @@ export async function loadChatSessions<Session>(
33
33
  }
34
34
 
35
35
  /** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
36
- export function useChatSessions<Session>(options: ChatSessionsOptions = {}) {
37
- const inbox = useAgent<InboxState<Session>>({
36
+ export function useChatSessions<
37
+ Session,
38
+ State extends InboxState<Session> = InboxState<Session>,
39
+ >(options: ChatSessionsOptions = {}) {
40
+ const inbox = useAgent<State>({
38
41
  agent: "Inbox",
39
42
  basePath: CURRENT_INBOX_AGENT_PATH,
40
43
  ...(options.protocols === undefined ? {} : { protocols: options.protocols }),
@@ -88,30 +91,18 @@ export function useChatSessions<Session>(options: ChatSessionsOptions = {}) {
88
91
  await inbox.call("archiveChat", [id, archived]);
89
92
  }, [inbox]);
90
93
 
91
- const reloadChatRuntime = useCallback(
92
- async (chatId: string, userAgentId: string) => {
93
- const result = await inbox.call<{ userAgentId: string } | null>(
94
- "chatReloadRuntime",
95
- [chatId, userAgentId],
96
- );
97
- if (!result) throw new Error("Session is unavailable");
98
- return result;
99
- },
100
- [inbox],
101
- );
102
-
103
94
  return {
104
95
  chats: inbox.state?.chats ?? snapshot ?? [],
105
96
  synced: inbox.state !== undefined || snapshot !== undefined,
106
97
  connected: inbox.identified,
107
98
  workspaceRev: inbox.state?.workspaceRev ?? 0,
99
+ inboxState: inbox.state,
108
100
  createChat,
109
101
  forkChat,
110
102
  renameChat,
111
103
  deleteChat,
112
104
  pinChat,
113
105
  archiveChat,
114
- reloadChatRuntime,
115
106
  };
116
107
  }
117
108
 
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export {
2
+ UniversalAgentChatProvider,
2
3
  useUniversalAgentChat,
3
4
  type AgentToolRun,
5
+ type ChatConnectionStatus,
4
6
  type ChatConnectionOptions,
5
7
  type ChatRuntime,
6
8
  } from "./use-universal-agent-chat";
@@ -21,4 +23,5 @@ export {
21
23
  useChatSessions,
22
24
  type ChatSessionsOptions,
23
25
  type ChatSessions,
26
+ type InboxState,
24
27
  } from "./chat-sessions";
@@ -1,13 +1,29 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ createContext,
3
+ createElement,
4
+ Suspense,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ReactNode,
12
+ } from "react";
2
13
  import { nanoid } from "nanoid";
3
14
  import type { ChatStatus, FileUIPart, UIMessage } from "ai";
4
15
  import type { AgentToolRunState } from "agents";
16
+ import type { AgentConnectionError } from "agents/client";
5
17
  import { MessageType } from "agents/chat";
6
18
  import { getAgentMessages, useAgentChat } from "agents/chat/react";
7
19
  import { useAgent, useAgentToolEvents } from "agents/react";
8
20
  import {
21
+ type ApprovalDecision,
9
22
  type MessageDelivery,
10
23
  type MessageDispatchReceipt,
24
+ type RequestedCapability,
25
+ type RuntimeAssemblyView,
26
+ type RuntimeConfigUpdateResult,
11
27
  type RuntimeQueuedSubmission,
12
28
  type RuntimeState,
13
29
  } from "@springbrand/agent-runtime/contracts";
@@ -15,9 +31,16 @@ import { createSafeUIMessageAgentConnection } from "./ui-message-stream-guard";
15
31
  import { CURRENT_INBOX_AGENT_PATH } from "./chat-sessions";
16
32
 
17
33
  type ChatMessageMetadata = Record<string, unknown> & {
34
+ error?: string;
35
+ requestedCapabilities?: readonly RequestedCapability[];
18
36
  turnStatus?: string;
19
37
  };
20
38
  type ChatMessage = UIMessage<ChatMessageMetadata>;
39
+ export type ChatConnectionStatus =
40
+ | "connecting"
41
+ | "connected"
42
+ | "rejected"
43
+ | "error";
21
44
  type FailedDispatch = {
22
45
  message: ChatMessage;
23
46
  delivery: MessageDelivery;
@@ -28,7 +51,7 @@ type ChatRuntimeLoadState =
28
51
  | { status: "idle"; available: false }
29
52
  | {
30
53
  status: "loading";
31
- phase: "config" | "plugins" | "mcp" | "pi";
54
+ phase: "config" | "assembly" | "mcp" | "pi";
32
55
  available: boolean;
33
56
  startedAt: number;
34
57
  updatedAt: number;
@@ -41,7 +64,7 @@ type ChatRuntimeLoadState =
41
64
  }
42
65
  | {
43
66
  status: "error";
44
- phase: "config" | "plugins" | "mcp" | "pi";
67
+ phase: "config" | "assembly" | "mcp" | "pi";
45
68
  available: boolean;
46
69
  startedAt: number;
47
70
  failedAt: number;
@@ -80,15 +103,60 @@ type ChatTurn = {
80
103
  export interface ChatRuntime {
81
104
  messages: ChatMessage[];
82
105
  status: ChatStatus;
106
+ connectionStatus: ChatConnectionStatus;
107
+ connectionError: AgentConnectionError | undefined;
83
108
  runtimeLoad: ChatRuntimeLoadState | undefined;
84
109
  isStreaming: boolean;
85
110
  error: string | Error | undefined;
86
- sendText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
87
- steerText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
88
- enqueueText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
111
+ sendText: (
112
+ text: string,
113
+ files?: readonly FileUIPart[],
114
+ capabilities?: readonly RequestedCapability[],
115
+ ) => Promise<boolean>;
116
+ steerText: (
117
+ text: string,
118
+ files?: readonly FileUIPart[],
119
+ capabilities?: readonly RequestedCapability[],
120
+ ) => Promise<boolean>;
121
+ enqueueText: (
122
+ text: string,
123
+ files?: readonly FileUIPart[],
124
+ capabilities?: readonly RequestedCapability[],
125
+ ) => Promise<boolean>;
89
126
  steerQueued: (submissionId: string) => Promise<boolean>;
90
127
  cancelQueued: (submissionId: string) => Promise<boolean>;
91
128
  stop: () => Promise<void>;
129
+ /**
130
+ * 把用户对某张交互卡片的操作回写成那次工具调用的结果。
131
+ *
132
+ * 走的是这条已开的 WS 连接(`agent.call`),不是 HTTP —— ack 与随后流下来的
133
+ * `tool-output-available` chunk 同序到达,UI 状态不会倒挂。
134
+ */
135
+ respondToolInteraction: (
136
+ toolCallId: string,
137
+ response: unknown,
138
+ ) => Promise<boolean>;
139
+ decideApproval: (
140
+ executionId: string,
141
+ decision: ApprovalDecision,
142
+ ) => Promise<{ ok: boolean }>;
143
+ /**
144
+ * 读取该 Session facet 当前真正装配出来的 Runtime。
145
+ *
146
+ * 复用这条已开的 WS 连接,调用方不必为一个只读控制面方法另建 `useAgent`。
147
+ *
148
+ * 与其它动作不同,失败会原样抛出:这是读取而不是提交,调用方需要知道是
149
+ * 装配未就绪还是连接断了,收敛成 `undefined` 会让调试面板无从显示原因。
150
+ */
151
+ getRuntimeAssembly: () => Promise<RuntimeAssemblyView>;
152
+ getRuntimeBinding: <Binding = unknown>() => Promise<Binding>;
153
+ readChatMetadata: <Metadata = unknown>() => Promise<Metadata>;
154
+ renameChat: <Metadata = unknown>(title: string) => Promise<Metadata>;
155
+ rebindRuntime: <Input = unknown, Result = unknown>(input: Input) => Promise<Result>;
156
+ updateConfig: <Change = unknown>(
157
+ command: unknown,
158
+ ) => Promise<RuntimeConfigUpdateResult<Change>>;
159
+ reloadRuntime: () => Promise<void>;
92
160
  regenerate: () => void;
93
161
  canRetry: boolean;
94
162
  agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[];
@@ -108,6 +176,8 @@ type SharedChatConnectionOptions = {
108
176
  protocols?: string | string[];
109
177
  inboxAgent?: string;
110
178
  sessionAgent?: string;
179
+ /** Host projection used only until the selected Session sends its first state. */
180
+ initialTurnActive?: boolean;
111
181
  };
112
182
 
113
183
  export type ChatConnectionOptions = SharedChatConnectionOptions & (
@@ -115,6 +185,51 @@ export type ChatConnectionOptions = SharedChatConnectionOptions & (
115
185
  | { inboxName?: never; basePath?: string }
116
186
  );
117
187
 
188
+ type SessionAgentConnection = ReturnType<typeof useAgent<RuntimeState>>;
189
+ type UniversalAgentChatContextValue = {
190
+ chatId: string;
191
+ connection: ChatConnectionOptions;
192
+ agent: SessionAgentConnection;
193
+ };
194
+
195
+ const UniversalAgentChatContext = createContext<UniversalAgentChatContextValue | null>(null);
196
+
197
+ export function UniversalAgentChatProvider({
198
+ chatId,
199
+ connection = {},
200
+ fallback = null,
201
+ children,
202
+ }: {
203
+ chatId: string;
204
+ connection?: ChatConnectionOptions;
205
+ fallback?: ReactNode;
206
+ children: ReactNode;
207
+ }) {
208
+ const inboxAgent = connection.inboxAgent ?? "Inbox";
209
+ const agent = useAgent<RuntimeState>({
210
+ agent: inboxAgent,
211
+ ...(connection.inboxName === undefined
212
+ ? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
213
+ : { name: connection.inboxName }),
214
+ ...(connection.host === undefined ? {} : { host: connection.host }),
215
+ ...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
216
+ connectionTimeout: 6_000,
217
+ sub: [{
218
+ agent: connection.sessionAgent ?? "UniversalAgent",
219
+ name: chatId,
220
+ }],
221
+ });
222
+ const value = useMemo(
223
+ () => ({ chatId, connection, agent }),
224
+ [agent, chatId, connection],
225
+ );
226
+ return createElement(
227
+ UniversalAgentChatContext.Provider,
228
+ { value },
229
+ createElement(Suspense, { fallback }, children),
230
+ );
231
+ }
232
+
118
233
  type ChatTurnTiming = {
119
234
  chatId: string;
120
235
  messageId: string;
@@ -170,10 +285,18 @@ function markChatTurnPhase(
170
285
  function createChatMessage(
171
286
  text: string,
172
287
  files?: readonly FileUIPart[],
288
+ capabilities: readonly RequestedCapability[] = [],
173
289
  ): ChatMessage | null {
174
290
  const normalizedText = text.trim();
175
291
  if (!normalizedText && !files?.length) return null;
176
292
  const createdAt = Date.now();
293
+ const seen = new Set<string>();
294
+ const requestedCapabilities = capabilities.filter((capability) => {
295
+ const key = `${capability.kind}:${capability.name}`;
296
+ if (seen.has(key)) return false;
297
+ seen.add(key);
298
+ return true;
299
+ });
177
300
  return {
178
301
  id: nanoid(),
179
302
  role: "user",
@@ -187,6 +310,7 @@ function createChatMessage(
187
310
  createdAt,
188
311
  authorDisplayName: "You",
189
312
  messageSource: "Web",
313
+ ...(requestedCapabilities.length > 0 ? { requestedCapabilities } : {}),
190
314
  },
191
315
  };
192
316
  }
@@ -233,10 +357,12 @@ function loadInitialMessages(
233
357
  * sub 数组由客户端 kebab 化;服务端按 ctx.exports 反解回 CamelCase className,
234
358
  * 与 Inbox.onBeforeSubAgent 的严格门卫(hasSubAgent)对齐。
235
359
  */
236
- export function useUniversalAgentChat(
237
- chatId: string,
238
- connection: ChatConnectionOptions = {},
239
- ): ChatRuntime {
360
+ export function useUniversalAgentChat(): ChatRuntime {
361
+ const context = useContext(UniversalAgentChatContext);
362
+ if (!context) {
363
+ throw new Error("useUniversalAgentChat must be used within UniversalAgentChatProvider");
364
+ }
365
+ const { chatId, connection, agent } = context;
240
366
  const [optimisticMessages, setOptimisticMessages] =
241
367
  useState<ChatMessage[]>([]);
242
368
  const [optimisticQueued, setOptimisticQueued] =
@@ -245,19 +371,6 @@ export function useUniversalAgentChat(
245
371
  const [canRetry, setCanRetry] = useState(false);
246
372
  const failedDispatchRef = useRef<FailedDispatch | undefined>(undefined);
247
373
  const activeSubmissionIdRef = useRef<string | undefined>(undefined);
248
- const inboxAgent = connection.inboxAgent ?? "Inbox";
249
- const agent = useAgent<RuntimeState>({
250
- agent: inboxAgent,
251
- ...(connection.inboxName === undefined
252
- ? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
253
- : { name: connection.inboxName }),
254
- ...(connection.host === undefined ? {} : { host: connection.host }),
255
- ...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
256
- sub: [{
257
- agent: connection.sessionAgent ?? "UniversalAgent",
258
- name: chatId,
259
- }],
260
- });
261
374
  const chatAgent = useMemo(
262
375
  () => createSafeUIMessageAgentConnection(agent),
263
376
  [agent],
@@ -281,9 +394,9 @@ export function useUniversalAgentChat(
281
394
  // autonomous responses 纪律(CF 文档采纳 2026-07-05):框架已提供三个流态标志,
282
395
  // 透出供 UI 区分「服务端主动流 vs 用户发起流」+「恢复态」(与 stall 看门狗配套)。
283
396
  isServerStreaming,
284
- isRecovering,
397
+ isRecovering: sdkIsRecovering,
285
398
  isToolContinuation,
286
- connectionError,
399
+ connectionError: sdkConnectionError,
287
400
  } = useAgentChat<RuntimeState, ChatMessage>({
288
401
  agent: chatAgent,
289
402
  getInitialMessages: ({ url }) =>
@@ -294,6 +407,9 @@ export function useUniversalAgentChat(
294
407
  syncMessagesToServer: false,
295
408
  throttle: 100,
296
409
  });
410
+ // The SDK keeps its advisory recovery flag until terminal settlement; a
411
+ // live server stream proves the recovered Turn is executing again.
412
+ const isRecovering = sdkIsRecovering && !isServerStreaming;
297
413
  const visibleMessages = useMemo(
298
414
  () => mergeOptimisticMessages(messages, optimisticMessages),
299
415
  [messages, optimisticMessages],
@@ -313,6 +429,18 @@ export function useUniversalAgentChat(
313
429
  ? "ready"
314
430
  : sdkStatus;
315
431
  const sdkError = stoppedByUser || hasTerminalMessage ? undefined : error;
432
+ const connectionError = sdkConnectionError ?? undefined;
433
+ const connectionStatus: ChatConnectionStatus = connectionError
434
+ ? connectionError.code === 1008 ? "rejected" : "error"
435
+ : agent.identified ? "connected" : "connecting";
436
+ const runtimeError = dispatchError ?? sdkError ?? connectionError;
437
+ const runtimeErrorMessage = runtimeError instanceof Error
438
+ ? runtimeError.message
439
+ : runtimeError;
440
+ const presentationError = latestTurnStatus === "error" &&
441
+ latestMessage?.metadata?.error === runtimeErrorMessage
442
+ ? undefined
443
+ : runtimeError;
316
444
  // RPC admissions bypass useChat's request lifecycle. Project the durable
317
445
  // Turn here so presentation stays correct without a second send path.
318
446
  const authoritativeStatus =
@@ -324,9 +452,12 @@ export function useUniversalAgentChat(
324
452
  ? "streaming"
325
453
  : "submitted"
326
454
  : normalizedSdkStatus;
455
+ const hydratingTurnActive = facetState === undefined &&
456
+ connection.initialTurnActive === true;
327
457
  const status = dispatchError
328
458
  ? "error"
329
- : optimisticMessages.length > 0 && authoritativeStatus === "ready"
459
+ : (optimisticMessages.length > 0 || hydratingTurnActive) &&
460
+ authoritativeStatus === "ready"
330
461
  ? "submitted"
331
462
  : authoritativeStatus;
332
463
  const messagesRef = useRef(messages);
@@ -418,7 +549,8 @@ export function useUniversalAgentChat(
418
549
  isToolContinuation;
419
550
  const pendingLocalTurn = optimisticMessages.length > 0 &&
420
551
  (status === "submitted" || status === "streaming");
421
- const turnActive = Boolean(turn?.activeSubmissionId) ||
552
+ const turnActive = hydratingTurnActive ||
553
+ Boolean(turn?.activeSubmissionId) ||
422
554
  pendingLocalTurn ||
423
555
  (authoritativeTurn === undefined && sdkTurnActive);
424
556
  const canSteer = turnActive &&
@@ -547,8 +679,9 @@ export function useUniversalAgentChat(
547
679
  const sendText = useCallback(async (
548
680
  text: string,
549
681
  files?: readonly FileUIPart[],
682
+ capabilities?: readonly RequestedCapability[],
550
683
  ): Promise<boolean> => {
551
- const message = createChatMessage(text, files);
684
+ const message = createChatMessage(text, files, capabilities);
552
685
  return message
553
686
  ? submitMessage(message, "enqueue", "message")
554
687
  : false;
@@ -557,21 +690,28 @@ export function useUniversalAgentChat(
557
690
  const dispatchText = useCallback(async (
558
691
  text: string,
559
692
  files: readonly FileUIPart[] | undefined,
693
+ capabilities: readonly RequestedCapability[] | undefined,
560
694
  delivery: MessageDelivery,
561
695
  projection: "message" | "queue",
562
696
  ): Promise<boolean> => {
563
- const message = createChatMessage(text, files);
697
+ const message = createChatMessage(text, files, capabilities);
564
698
  return message ? submitMessage(message, delivery, projection) : false;
565
699
  }, [submitMessage]);
566
700
 
567
701
  const steerText = useCallback(
568
- (text: string, files?: readonly FileUIPart[]) =>
569
- dispatchText(text, files, "steer", "message"),
702
+ (
703
+ text: string,
704
+ files?: readonly FileUIPart[],
705
+ capabilities?: readonly RequestedCapability[],
706
+ ) => dispatchText(text, files, capabilities, "steer", "message"),
570
707
  [dispatchText],
571
708
  );
572
709
  const enqueueText = useCallback(
573
- (text: string, files?: readonly FileUIPart[]) =>
574
- dispatchText(text, files, "enqueue", "queue"),
710
+ (
711
+ text: string,
712
+ files?: readonly FileUIPart[],
713
+ capabilities?: readonly RequestedCapability[],
714
+ ) => dispatchText(text, files, capabilities, "enqueue", "queue"),
575
715
  [dispatchText],
576
716
  );
577
717
  const steerQueued = useCallback(async (submissionId: string) => {
@@ -664,13 +804,88 @@ export function useUniversalAgentChat(
664
804
  );
665
805
  }, [submitMessage]);
666
806
 
807
+ // 交互卡片的写回口。失败只返回 false —— 调用方(块组件)自己决定怎么提示;
808
+ // 权威状态永远来自 part 的 state,不来自这里的返回值。
809
+ const respondToolInteraction = useCallback(async (
810
+ toolCallId: string,
811
+ response: unknown,
812
+ ): Promise<boolean> => {
813
+ try {
814
+ const receipt = await agentRef.current.call<{ ok: boolean }>(
815
+ "respondToolInteraction",
816
+ [toolCallId, response],
817
+ );
818
+ return receipt.ok;
819
+ } catch {
820
+ return false;
821
+ }
822
+ }, []);
823
+ const decideApproval = useCallback(
824
+ (executionId: string, decision: ApprovalDecision) =>
825
+ agentRef.current.call<{ ok: boolean }>(
826
+ "decideApproval",
827
+ [executionId, decision],
828
+ ),
829
+ [],
830
+ );
831
+
832
+ // 只读控制面:Runtime 尚未装配时由 Agent 侧 `ensureRuntimeReady` 负责等待,
833
+ // 这里不缓存结果 —— 换模型、加技能或 reload 之后调用方要拿到的是新装配。
834
+ const getRuntimeAssembly = useCallback(
835
+ () => agentRef.current.call<RuntimeAssemblyView>("getRuntimeAssembly", []),
836
+ [],
837
+ );
838
+ const getRuntimeBinding = useCallback(
839
+ <Binding,>() => agentRef.current.call<Binding>("getRuntimeBinding", []),
840
+ [],
841
+ );
842
+ const readChatMetadata = useCallback(
843
+ <Metadata,>() => agentRef.current.call<Metadata>("readChatMetadata", []),
844
+ [],
845
+ );
846
+ const renameChat = useCallback(
847
+ <Metadata,>(title: string) =>
848
+ agentRef.current.call<Metadata>("renameChat", [title]),
849
+ [],
850
+ );
851
+ const rebindRuntime = useCallback(
852
+ <Input, Result,>(input: Input) =>
853
+ agentRef.current.call<Result>("rebindRuntime", [input]),
854
+ [],
855
+ );
856
+ const updateConfig = useCallback(
857
+ <Change,>(command: unknown) =>
858
+ agentRef.current.call<RuntimeConfigUpdateResult<Change>>(
859
+ "updateConfig",
860
+ [command],
861
+ ),
862
+ [],
863
+ );
864
+ const reloadRuntime = useCallback(
865
+ () => agentRef.current.call<void>(
866
+ "reloadRuntime",
867
+ [undefined, { force: true }],
868
+ ),
869
+ [],
870
+ );
871
+
667
872
  return {
873
+ respondToolInteraction,
874
+ decideApproval,
875
+ getRuntimeAssembly,
876
+ getRuntimeBinding,
877
+ readChatMetadata,
878
+ renameChat,
879
+ rebindRuntime,
880
+ updateConfig,
881
+ reloadRuntime,
668
882
  messages: visibleMessages,
669
883
  status,
884
+ connectionStatus,
885
+ connectionError,
670
886
  runtimeLoad: facetState?.runtimeLoad,
671
887
  isStreaming,
672
- error: dispatchError ?? sdkError ??
673
- connectionError ?? undefined,
888
+ error: presentationError,
674
889
  sendText,
675
890
  steerText,
676
891
  enqueueText,