@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 +5 -5
- package/dist/index.js +76 -28
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
|
|
2
|
-
import { NcpAgentClientEndpoint, NcpAgentConversationSnapshot, NcpMessage, NcpMessagePart
|
|
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 |
|
|
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
|
|
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
|
|
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
|
-
}):
|
|
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:
|
|
89
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
88
90
|
message: {
|
|
89
91
|
...input.message,
|
|
90
|
-
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
253
|
+
if (isStaleHydrationRequest(loadStateRef.current, requestId, controller)) return;
|
|
202
254
|
manager.hydrate({
|
|
203
255
|
sessionId,
|
|
204
256
|
messages: seed.messages,
|
|
205
|
-
activeRun: seed
|
|
206
|
-
runId: null,
|
|
207
|
-
sessionId,
|
|
208
|
-
abortDisabledReason: null
|
|
209
|
-
} : null
|
|
257
|
+
activeRun: createHydratedActiveRun(seed, sessionId)
|
|
210
258
|
});
|
|
211
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
19
|
-
"@nextclaw/ncp-toolkit": "0.5.
|
|
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"
|