@springbrand/chat-client 0.1.1 → 0.1.3-alpha.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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@springbrand/chat-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.3-alpha.0",
4
4
  "type": "module",
5
5
  "files": [
6
- "src"
6
+ "src",
7
+ "!src/**/*.test.ts"
7
8
  ],
8
9
  "publishConfig": {
9
10
  "access": "public"
@@ -17,14 +18,14 @@
17
18
  "react": "^19.0.0"
18
19
  },
19
20
  "dependencies": {
20
- "nanoid": "^5.1.16",
21
- "@springbrand/agent-runtime": "0.1.1"
21
+ "nanoid": "^5.1.16"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/react": "^19.2.17",
25
25
  "@types/react-dom": "^19.2.3",
26
26
  "react-dom": "^19.2.7",
27
- "typescript": "^7.0.2"
27
+ "typescript": "^7.0.2",
28
+ "@springbrand/agent-runtime": "0.1.3-alpha.1"
28
29
  },
29
30
  "scripts": {
30
31
  "typecheck": "tsc --noEmit"
@@ -1,6 +1,5 @@
1
- import { useCallback, useRef } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { useAgent } from "agents/react";
3
- import type { RuntimeActivity } from "@springbrand/agent-runtime/contracts";
4
3
 
5
4
  export interface ChatSessionSummary {
6
5
  id: string;
@@ -11,7 +10,7 @@ export interface ChatSessionSummary {
11
10
  updatedAt: number;
12
11
  pinned: boolean;
13
12
  archived: boolean;
14
- activity: RuntimeActivity;
13
+ activity: "idle" | "working" | "needs-input";
15
14
  usage?: {
16
15
  totalTokens: number;
17
16
  totalCost: number;
@@ -25,18 +24,49 @@ interface InboxState {
25
24
 
26
25
  export const CURRENT_INBOX_AGENT_PATH = "api/agent-connections/inbox";
27
26
 
27
+ type Request = typeof fetch;
28
+
29
+ export async function loadChatSessions(
30
+ signal?: AbortSignal,
31
+ request: Request = fetch,
32
+ ): Promise<ChatSessionSummary[]> {
33
+ const response = await request("/api/chats", {
34
+ cache: "no-store",
35
+ headers: { Accept: "application/json" },
36
+ signal,
37
+ });
38
+ if (!response.ok) throw new Error("Unable to load chat sessions");
39
+ const body = await response.json().catch(() => null) as
40
+ | { chats?: unknown }
41
+ | null;
42
+ if (!Array.isArray(body?.chats)) {
43
+ throw new Error("Invalid chat sessions response");
44
+ }
45
+ return body.chats as ChatSessionSummary[];
46
+ }
47
+
28
48
  /** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
29
49
  export function useChatSessions() {
30
50
  const inbox = useAgent<InboxState>({
31
51
  agent: "Inbox",
32
52
  basePath: CURRENT_INBOX_AGENT_PATH,
33
53
  });
54
+ const [snapshot, setSnapshot] = useState<ChatSessionSummary[]>();
34
55
  const creatingRef = useRef(false);
35
56
 
57
+ useEffect(() => {
58
+ const controller = new AbortController();
59
+ void loadChatSessions(controller.signal)
60
+ .then(setSnapshot)
61
+ .catch(() => undefined); // WebSocket state remains the fallback.
62
+ return () => controller.abort();
63
+ }, []);
64
+
36
65
  const createChat = useCallback(async (userAgentId?: string) => {
37
66
  if (creatingRef.current) return undefined;
38
67
  creatingRef.current = true;
39
68
  try {
69
+ await inbox.ready;
40
70
  return await inbox.call<ChatSessionSummary>(
41
71
  "createChat",
42
72
  userAgentId ? [{ userAgentId }] : undefined,
@@ -83,8 +113,9 @@ export function useChatSessions() {
83
113
  );
84
114
 
85
115
  return {
86
- chats: inbox.state?.chats ?? [],
87
- synced: inbox.state !== undefined,
116
+ chats: inbox.state?.chats ?? snapshot ?? [],
117
+ synced: inbox.state !== undefined || snapshot !== undefined,
118
+ connected: inbox.identified,
88
119
  workspaceRev: inbox.state?.workspaceRev ?? 0,
89
120
  createChat,
90
121
  forkChat,
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  useUniversalAgentChat,
3
3
  type AgentToolRun,
4
+ type ChatConnectionOptions,
4
5
  type ChatRuntime,
5
6
  } from "./use-universal-agent-chat";
6
7
  export {
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { nanoid } from "nanoid";
3
- import type { FileUIPart, UIMessage } from "ai";
4
- import { useAgentChat } from "agents/chat/react";
3
+ import type { ChatStatus, FileUIPart, UIMessage } from "ai";
4
+ import type { AgentToolRunState } from "agents";
5
+ import { getAgentMessages, useAgentChat } from "agents/chat/react";
5
6
  import { useAgent, useAgentToolEvents } from "agents/react";
6
7
  import {
7
- USER_STOP_REASON,
8
8
  type MessageDelivery,
9
9
  type MessageDispatchReceipt,
10
10
  type RuntimeQueuedSubmission,
@@ -23,6 +23,92 @@ type FailedDispatch = {
23
23
  projection: "message" | "queue";
24
24
  };
25
25
 
26
+ type ChatRuntimeLoadState =
27
+ | { status: "idle"; available: false }
28
+ | {
29
+ status: "loading";
30
+ phase: "config" | "plugins" | "mcp" | "pi";
31
+ available: boolean;
32
+ startedAt: number;
33
+ updatedAt: number;
34
+ }
35
+ | {
36
+ status: "ready";
37
+ available: true;
38
+ startedAt: number;
39
+ completedAt: number;
40
+ }
41
+ | {
42
+ status: "error";
43
+ phase: "config" | "plugins" | "mcp" | "pi";
44
+ available: boolean;
45
+ startedAt: number;
46
+ failedAt: number;
47
+ };
48
+
49
+ type ChatApproval = {
50
+ executionId: string;
51
+ source: "action" | "codemode" | "temporary-agent";
52
+ action: string;
53
+ summary: string;
54
+ executionLevel: "safe" | "low" | "medium" | "high";
55
+ requiredExecutionLevel: "safe" | "low" | "medium" | "high";
56
+ inputJson: string;
57
+ requestId: string;
58
+ };
59
+
60
+ type ChatQueuedSubmission = {
61
+ submissionId: string;
62
+ messageId: string;
63
+ preview: string;
64
+ position: number;
65
+ createdAt: number;
66
+ };
67
+
68
+ type ChatTurn = {
69
+ activeSubmissionId?: string;
70
+ steerable: boolean;
71
+ hasPendingSteer: boolean;
72
+ queued: ChatQueuedSubmission[];
73
+ };
74
+
75
+ export interface ChatRuntime {
76
+ messages: ChatMessage[];
77
+ status: ChatStatus;
78
+ runtimeLoad: ChatRuntimeLoadState | undefined;
79
+ isStreaming: boolean;
80
+ error: string | Error | undefined;
81
+ sendText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
82
+ steerText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
83
+ enqueueText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
84
+ steerQueued: (submissionId: string) => Promise<boolean>;
85
+ cancelQueued: (submissionId: string) => Promise<boolean>;
86
+ stop: () => Promise<void>;
87
+ regenerate: () => void;
88
+ canRetry: boolean;
89
+ agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[];
90
+ isServerStreaming: boolean;
91
+ isRecovering: boolean;
92
+ isToolContinuation: boolean;
93
+ approvals: ChatApproval[] | undefined;
94
+ approvalsLoaded: boolean;
95
+ turn: ChatTurn | undefined;
96
+ turnActive: boolean;
97
+ canSteer: boolean;
98
+ }
99
+
100
+ type SharedChatConnectionOptions = {
101
+ host?: string;
102
+ credentials?: RequestCredentials;
103
+ inboxAgent?: string;
104
+ sessionAgent?: string;
105
+ };
106
+
107
+ export type ChatConnectionOptions = SharedChatConnectionOptions & (
108
+ | { inboxName: string; basePath?: never }
109
+ | { inboxName?: never; basePath?: string }
110
+ );
111
+
26
112
  type ChatTurnTiming = {
27
113
  chatId: string;
28
114
  messageId: string;
@@ -47,6 +133,8 @@ const CHAT_TURN_SEGMENTS = {
47
133
  ready: ["firstText", "universal-agent.chat.first_text_to_ready"],
48
134
  } as const;
49
135
 
136
+ const USER_STOP_REASON = "Stopped by user";
137
+
50
138
  function now(): number {
51
139
  return typeof performance === "undefined" ? Date.now() : performance.now();
52
140
  }
@@ -115,6 +203,19 @@ function queuedPreview(message: ChatMessage): string {
115
203
  }).join(" ").trim().slice(0, 160) || "Queued message";
116
204
  }
117
205
 
206
+ function loadInitialMessages(
207
+ url: string | undefined,
208
+ credentials: RequestCredentials | undefined,
209
+ ): Promise<ChatMessage[]> {
210
+ if (!url) return Promise.resolve([]);
211
+ const messagesUrl = new URL(url);
212
+ messagesUrl.pathname = `${messagesUrl.pathname.replace(/\/$/, "")}/get-messages`;
213
+ return getAgentMessages({
214
+ url: messagesUrl.toString(),
215
+ ...(credentials === undefined ? {} : { credentials }),
216
+ }) as Promise<ChatMessage[]>;
217
+ }
218
+
118
219
  /**
119
220
  * 薄适配层:把 useAgent + UIMessage chat + useAgentToolEvents
120
221
  * 收敛成一个归一化 runtime,对外只暴露 messages / 发送 / 状态 / 子 agent 运行,
@@ -126,7 +227,10 @@ function queuedPreview(message: ChatMessage): string {
126
227
  * sub 数组由客户端 kebab 化;服务端按 ctx.exports 反解回 CamelCase className,
127
228
  * 与 Inbox.onBeforeSubAgent 的严格门卫(hasSubAgent)对齐。
128
229
  */
129
- export function useUniversalAgentChat(chatId: string) {
230
+ export function useUniversalAgentChat(
231
+ chatId: string,
232
+ connection: ChatConnectionOptions = {},
233
+ ): ChatRuntime {
130
234
  const [optimisticMessages, setOptimisticMessages] =
131
235
  useState<ChatMessage[]>([]);
132
236
  const [optimisticQueued, setOptimisticQueued] =
@@ -135,10 +239,17 @@ export function useUniversalAgentChat(chatId: string) {
135
239
  const [canRetry, setCanRetry] = useState(false);
136
240
  const failedDispatchRef = useRef<FailedDispatch | undefined>(undefined);
137
241
  const activeSubmissionIdRef = useRef<string | undefined>(undefined);
242
+ const inboxAgent = connection.inboxAgent ?? "Inbox";
138
243
  const agent = useAgent<RuntimeState>({
139
- agent: "Inbox",
140
- basePath: CURRENT_INBOX_AGENT_PATH,
141
- sub: [{ agent: "UniversalAgent", name: chatId }],
244
+ agent: inboxAgent,
245
+ ...(connection.inboxName === undefined
246
+ ? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
247
+ : { name: connection.inboxName }),
248
+ ...(connection.host === undefined ? {} : { host: connection.host }),
249
+ sub: [{
250
+ agent: connection.sessionAgent ?? "UniversalAgent",
251
+ name: chatId,
252
+ }],
142
253
  });
143
254
  const chatAgent = useMemo(
144
255
  () => createSafeUIMessageAgentConnection(agent),
@@ -167,6 +278,11 @@ export function useUniversalAgentChat(chatId: string) {
167
278
  connectionError,
168
279
  } = useAgentChat<RuntimeState, ChatMessage>({
169
280
  agent: chatAgent,
281
+ getInitialMessages: ({ url }) =>
282
+ loadInitialMessages(url, connection.credentials),
283
+ ...(connection.credentials === undefined
284
+ ? {}
285
+ : { credentials: connection.credentials }),
170
286
  syncMessagesToServer: false,
171
287
  throttle: 100,
172
288
  });
@@ -287,8 +403,10 @@ export function useUniversalAgentChat(chatId: string) {
287
403
 
288
404
  // 子 agent 运行(agentTool 前台 + runAgentTool detached 后台)的实时事件投影:
289
405
  // 状态机 + progress snapshot + durable milestones(knowledge/upstream-adoption.csv F6)
290
- const { runsById } = useAgentToolEvents({ agent });
291
- const agentToolRuns = useMemo(
406
+ const { runsById } = useAgentToolEvents<ChatMessage["parts"][number]>({
407
+ agent,
408
+ });
409
+ const agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[] = useMemo(
292
410
  () => Object.values(runsById).sort((a, b) => a.order - b.order),
293
411
  [runsById],
294
412
  );
@@ -538,5 +656,4 @@ export function useUniversalAgentChat(chatId: string) {
538
656
  };
539
657
  }
540
658
 
541
- export type ChatRuntime = ReturnType<typeof useUniversalAgentChat>;
542
659
  export type AgentToolRun = ChatRuntime["agentToolRuns"][number];