@nextclaw/ncp-react 0.4.28 → 0.4.29

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/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
2
- import { NcpAgentClientEndpoint, NcpAgentConversationSnapshot, NcpMessage, NcpMessagePart, NcpRequestEnvelope } from "@nextclaw/ncp";
2
+ import { NcpAgentClientEndpoint, NcpAgentConversationSnapshot, NcpAgentSendEnvelope, NcpMessage, NcpMessagePart } from "@nextclaw/ncp";
3
3
 
4
4
  //#region src/hooks/use-ncp-agent-runtime.d.ts
5
- type NcpAgentSendInput = string | NcpRequestEnvelope;
5
+ type NcpAgentSendInput = string | NcpAgentSendEnvelope;
6
6
  type UseNcpAgentResult = {
7
7
  snapshot: NcpAgentConversationSnapshot;
8
8
  visibleMessages: readonly NcpMessage[];
@@ -21,7 +21,7 @@ type NcpConversationSeed = {
21
21
  };
22
22
  type NcpConversationSeedLoader = (sessionId: string, signal: AbortSignal) => Promise<NcpConversationSeed>;
23
23
  type UseHydratedNcpAgentOptions = {
24
- sessionId: string;
24
+ sessionId?: string;
25
25
  client: NcpAgentClientEndpoint;
26
26
  loadSeed: NcpConversationSeedLoader;
27
27
  };
@@ -72,14 +72,14 @@ type ReadNcpDraftAttachmentsResult = {
72
72
  };
73
73
  declare function buildNcpImageAttachmentDataUrl(attachment: NcpDraftAttachment): string;
74
74
  declare function buildNcpRequestEnvelope(params: {
75
- sessionId: string;
75
+ sessionId?: string;
76
76
  text?: string;
77
77
  attachments?: readonly NcpDraftAttachment[];
78
78
  parts?: readonly NcpMessagePart[];
79
79
  metadata?: Record<string, unknown>;
80
80
  messageId?: string;
81
81
  timestamp?: string;
82
- }): NcpRequestEnvelope | null;
82
+ }): NcpAgentSendEnvelope | null;
83
83
  declare function readFilesAsNcpDraftAttachments(files: Iterable<File>, options?: ReadNcpDraftAttachmentsOptions): Promise<ReadNcpDraftAttachmentsResult>;
84
84
  declare function uploadFilesAsNcpDraftAttachments(files: Iterable<File>, options: UploadNcpDraftAttachmentsOptions): Promise<ReadNcpDraftAttachmentsResult>;
85
85
  //#endregion
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ function dispatchEventsToManager(manager, events) {
51
51
  return events.reduce((chain, event) => chain.then(() => manager.dispatch(event)), Promise.resolve());
52
52
  }
53
53
  function shouldDispatchEventToSession(event, sessionId) {
54
+ if (!sessionId) return true;
54
55
  const payload = "payload" in event ? event.payload : null;
55
56
  if (!payload || typeof payload !== "object") return true;
56
57
  if (!("sessionId" in payload) || typeof payload.sessionId !== "string") return true;
@@ -67,10 +68,10 @@ function normalizeSendEnvelope(input, sessionId) {
67
68
  const content = input.trim();
68
69
  if (!content) return null;
69
70
  return {
70
- sessionId,
71
+ ...sessionId ? { sessionId } : {},
71
72
  message: {
72
73
  id: `user-${Date.now().toString(36)}`,
73
- sessionId,
74
+ ...sessionId ? { sessionId } : {},
74
75
  role: "user",
75
76
  status: "final",
76
77
  parts: [{
@@ -82,33 +83,43 @@ function normalizeSendEnvelope(input, sessionId) {
82
83
  };
83
84
  }
84
85
  if (!hasMessageContent(input.message)) return null;
86
+ const targetSessionId = input.sessionId || input.message.sessionId || sessionId;
85
87
  return {
86
88
  ...input,
87
- sessionId: input.sessionId,
89
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
88
90
  message: {
89
91
  ...input.message,
90
- sessionId: input.message.sessionId || input.sessionId
92
+ ...targetSessionId ? { sessionId: targetSessionId } : {}
91
93
  }
92
94
  };
93
95
  }
94
96
  function useScopedAgentManager(sessionId) {
95
97
  const managerRef = useRef();
96
- if (!managerRef.current || managerRef.current.sessionId !== sessionId) managerRef.current = {
97
- sessionId,
98
+ if (!managerRef.current) managerRef.current = {
99
+ sessionId: sessionId ?? null,
100
+ manager: new DefaultNcpAgentConversationStateManager()
101
+ };
102
+ else if (sessionId && managerRef.current.sessionId === null) managerRef.current.sessionId = sessionId;
103
+ else if (managerRef.current.sessionId !== (sessionId ?? null)) managerRef.current = {
104
+ sessionId: sessionId ?? null,
98
105
  manager: new DefaultNcpAgentConversationStateManager()
99
106
  };
100
107
  return managerRef.current.manager;
101
108
  }
102
109
  function useNcpAgentRuntime({ sessionId, client, manager }) {
110
+ const sessionIdRef = useRef(sessionId);
103
111
  const snapshot = useSyncExternalStore((onStoreChange) => manager.subscribe(() => onStoreChange()), () => manager.getSnapshot(), () => manager.getSnapshot());
104
112
  const [isSending, setIsSending] = useState(false);
113
+ useEffect(() => {
114
+ sessionIdRef.current = sessionId;
115
+ }, [sessionId]);
105
116
  useEffect(() => {
106
117
  setIsSending(false);
107
118
  }, [sessionId]);
108
119
  useEffect(() => {
109
120
  const eventBatcher = new NcpEventDispatchBatcher((events) => dispatchEventsToManager(manager, events));
110
121
  const unsubscribeClient = client.subscribe((event) => {
111
- if (!shouldDispatchEventToSession(event, sessionId)) return;
122
+ if (!shouldDispatchEventToSession(event, sessionIdRef.current)) return;
112
123
  eventBatcher.enqueue(event);
113
124
  });
114
125
  return () => {
@@ -116,11 +127,7 @@ function useNcpAgentRuntime({ sessionId, client, manager }) {
116
127
  eventBatcher.dispose();
117
128
  client.stop();
118
129
  };
119
- }, [
120
- client,
121
- manager,
122
- sessionId
123
- ]);
130
+ }, [client, manager]);
124
131
  const visibleMessages = snapshot.streamingMessage ? [...snapshot.messages, snapshot.streamingMessage] : snapshot.messages;
125
132
  const activeRunId = snapshot.activeRun?.runId ?? null;
126
133
  const isRunning = !!snapshot.activeRun;
@@ -129,11 +136,14 @@ function useNcpAgentRuntime({ sessionId, client, manager }) {
129
136
  const envelope = normalizeSendEnvelope(input, sessionId);
130
137
  if (!envelope) return;
131
138
  setIsSending(true);
132
- await manager.dispatch({
139
+ if (sessionId) await manager.dispatch({
133
140
  type: NcpEventType.MessageSent,
134
141
  payload: {
135
142
  sessionId,
136
- message: envelope.message,
143
+ message: {
144
+ ...envelope.message,
145
+ sessionId
146
+ },
137
147
  metadata: envelope.metadata
138
148
  }
139
149
  });
@@ -144,10 +154,11 @@ function useNcpAgentRuntime({ sessionId, client, manager }) {
144
154
  }
145
155
  };
146
156
  const abort = async () => {
147
- if (!snapshot.activeRun) return;
157
+ if (!snapshot.activeRun || !sessionId) return;
148
158
  await client.abort({ sessionId });
149
159
  };
150
160
  const streamRun = async () => {
161
+ if (!sessionId) return;
151
162
  await client.stop();
152
163
  await client.stream({ sessionId });
153
164
  };
@@ -168,7 +179,23 @@ function toError(error) {
168
179
  return error instanceof Error ? error : new Error(String(error));
169
180
  }
170
181
  function resolveSessionHydratingState(params) {
171
- return params.isHydrating || params.hydratedSessionId !== params.sessionId;
182
+ const { hydratedSessionId, isHydrating, sessionId } = params;
183
+ if (!sessionId) return false;
184
+ return isHydrating || hydratedSessionId !== sessionId;
185
+ }
186
+ function managerAlreadyHasSessionState(manager, sessionId) {
187
+ const snapshot = manager.getSnapshot();
188
+ return snapshot.activeRun?.sessionId === sessionId || snapshot.streamingMessage?.sessionId === sessionId || snapshot.messages.some((message) => message.sessionId === sessionId);
189
+ }
190
+ function createHydratedActiveRun(seed, sessionId) {
191
+ return seed.status === "running" ? {
192
+ runId: null,
193
+ sessionId,
194
+ abortDisabledReason: null
195
+ } : null;
196
+ }
197
+ function isStaleHydrationRequest(loadState, requestId, controller) {
198
+ return controller.signal.aborted || loadState.requestId !== requestId;
172
199
  }
173
200
  function useHydratedNcpAgent({ sessionId, client, loadSeed }) {
174
201
  const manager = useScopedAgentManager(sessionId);
@@ -184,39 +211,58 @@ function useHydratedNcpAgent({ sessionId, client, loadSeed }) {
184
211
  requestId: 0,
185
212
  controller: null
186
213
  });
214
+ const resetEmptySession = useCallback(async () => {
215
+ await client.stop();
216
+ manager.reset();
217
+ setHydrateError(null);
218
+ setHydratedSessionId(null);
219
+ setIsHydrating(false);
220
+ loadStateRef.current = {
221
+ requestId: loadStateRef.current.requestId + 1,
222
+ controller: null
223
+ };
224
+ }, [client, manager]);
225
+ const markHydratedFromLiveState = useCallback((targetSessionId) => {
226
+ setHydrateError(null);
227
+ setHydratedSessionId(targetSessionId);
228
+ setIsHydrating(false);
229
+ }, []);
187
230
  const hydrateSeed = useCallback(async () => {
188
231
  loadStateRef.current.controller?.abort();
232
+ if (!sessionId) {
233
+ await resetEmptySession();
234
+ return;
235
+ }
189
236
  const controller = new AbortController();
190
237
  const requestId = loadStateRef.current.requestId + 1;
191
238
  loadStateRef.current = {
192
239
  requestId,
193
240
  controller
194
241
  };
242
+ if (managerAlreadyHasSessionState(manager, sessionId)) {
243
+ markHydratedFromLiveState(sessionId);
244
+ if (loadStateRef.current.controller === controller) loadStateRef.current.controller = null;
245
+ return;
246
+ }
195
247
  await client.stop();
196
248
  manager.reset();
197
249
  setHydrateError(null);
198
250
  setIsHydrating(true);
199
251
  try {
200
252
  const seed = await loadSeed(sessionId, controller.signal);
201
- if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) return;
253
+ if (isStaleHydrationRequest(loadStateRef.current, requestId, controller)) return;
202
254
  manager.hydrate({
203
255
  sessionId,
204
256
  messages: seed.messages,
205
- activeRun: seed.status === "running" ? {
206
- runId: null,
207
- sessionId,
208
- abortDisabledReason: null
209
- } : null
257
+ activeRun: createHydratedActiveRun(seed, sessionId)
210
258
  });
211
- setHydrateError(null);
212
- setHydratedSessionId(sessionId);
213
- setIsHydrating(false);
259
+ markHydratedFromLiveState(sessionId);
214
260
  client.stream({ sessionId }).catch((error) => {
215
261
  if (loadStateRef.current.requestId !== requestId) return;
216
262
  setHydrateError(toError(error));
217
263
  });
218
264
  } catch (error) {
219
- if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) return;
265
+ if (isStaleHydrationRequest(loadStateRef.current, requestId, controller)) return;
220
266
  setHydrateError(toError(error));
221
267
  setHydratedSessionId(sessionId);
222
268
  setIsHydrating(false);
@@ -227,6 +273,8 @@ function useHydratedNcpAgent({ sessionId, client, loadSeed }) {
227
273
  client,
228
274
  loadSeed,
229
275
  manager,
276
+ markHydratedFromLiveState,
277
+ resetEmptySession,
230
278
  sessionId
231
279
  ]);
232
280
  useEffect(() => {
@@ -341,10 +389,10 @@ function buildNcpRequestEnvelope(params) {
341
389
  const timestamp = params.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
342
390
  const messageId = params.messageId ?? `user-${Date.now().toString(36)}`;
343
391
  return {
344
- sessionId: params.sessionId,
392
+ ...params.sessionId ? { sessionId: params.sessionId } : {},
345
393
  message: {
346
394
  id: messageId,
347
- sessionId: params.sessionId,
395
+ ...params.sessionId ? { sessionId: params.sessionId } : {},
348
396
  role: "user",
349
397
  status: "final",
350
398
  parts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-react",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "private": false,
5
5
  "description": "React bindings for building NCP-based agent applications.",
6
6
  "type": "module",
@@ -15,8 +15,8 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.5.8",
19
- "@nextclaw/ncp-toolkit": "0.5.13"
18
+ "@nextclaw/ncp": "0.5.9",
19
+ "@nextclaw/ncp-toolkit": "0.5.14"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": "^18.0.0 || ^19.0.0"