@nextclaw/ncp-react 0.4.15 → 0.4.16
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 +54 -45
- package/dist/index.js +383 -438
- package/package.json +3 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,77 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
|
|
2
|
+
import { NcpAgentClientEndpoint, NcpAgentConversationSnapshot, NcpMessage, NcpMessagePart, NcpRequestEnvelope } from "@nextclaw/ncp";
|
|
2
3
|
|
|
4
|
+
//#region src/hooks/use-ncp-agent-runtime.d.ts
|
|
3
5
|
type NcpAgentSendInput = string | NcpRequestEnvelope;
|
|
4
6
|
type UseNcpAgentResult = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
snapshot: NcpAgentConversationSnapshot;
|
|
8
|
+
visibleMessages: readonly NcpMessage[];
|
|
9
|
+
activeRunId: string | null;
|
|
10
|
+
isRunning: boolean;
|
|
11
|
+
isSending: boolean;
|
|
12
|
+
send: (input: NcpAgentSendInput) => Promise<void>;
|
|
13
|
+
abort: () => Promise<void>;
|
|
14
|
+
streamRun: () => Promise<void>;
|
|
13
15
|
};
|
|
14
|
-
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/hooks/use-hydrated-ncp-agent.d.ts
|
|
15
18
|
type NcpConversationSeed = {
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
messages: readonly NcpMessage[];
|
|
20
|
+
status: "idle" | "running";
|
|
18
21
|
};
|
|
19
22
|
type NcpConversationSeedLoader = (sessionId: string, signal: AbortSignal) => Promise<NcpConversationSeed>;
|
|
20
23
|
type UseHydratedNcpAgentOptions = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
sessionId: string;
|
|
25
|
+
client: NcpAgentClientEndpoint;
|
|
26
|
+
loadSeed: NcpConversationSeedLoader;
|
|
24
27
|
};
|
|
25
28
|
type UseHydratedNcpAgentResult = UseNcpAgentResult & {
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
isHydrating: boolean;
|
|
30
|
+
hydrateError: Error | null;
|
|
28
31
|
};
|
|
29
|
-
declare function useHydratedNcpAgent({
|
|
30
|
-
|
|
32
|
+
declare function useHydratedNcpAgent({
|
|
33
|
+
sessionId,
|
|
34
|
+
client,
|
|
35
|
+
loadSeed
|
|
36
|
+
}: UseHydratedNcpAgentOptions): UseHydratedNcpAgentResult;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/hooks/use-ncp-agent.d.ts
|
|
31
39
|
declare function useNcpAgent(sessionId: string, client: NcpAgentClientEndpoint): UseNcpAgentResult;
|
|
32
|
-
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/attachments/ncp-attachments.d.ts
|
|
33
42
|
declare const DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT = "image/png,image/jpeg,image/webp,image/gif";
|
|
34
43
|
declare const DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
35
44
|
declare const DEFAULT_NCP_ATTACHMENT_MAX_BYTES: number;
|
|
36
45
|
declare const DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES: number;
|
|
37
46
|
declare const DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE = "application/octet-stream";
|
|
38
47
|
type NcpDraftAttachment = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
mimeType: string;
|
|
51
|
+
sizeBytes: number;
|
|
52
|
+
assetUri?: string;
|
|
53
|
+
url?: string;
|
|
54
|
+
contentBase64?: string;
|
|
46
55
|
};
|
|
47
56
|
type NcpRejectedAttachment = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
fileName: string;
|
|
58
|
+
mimeType: string;
|
|
59
|
+
sizeBytes: number;
|
|
60
|
+
reason: "unsupported-type" | "too-large" | "read-failed";
|
|
52
61
|
};
|
|
53
62
|
type ReadNcpDraftAttachmentsOptions = {
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
acceptedMimeTypes?: readonly string[];
|
|
64
|
+
maxBytes?: number;
|
|
56
65
|
};
|
|
57
66
|
type UploadNcpDraftAttachmentsOptions = ReadNcpDraftAttachmentsOptions & {
|
|
58
|
-
|
|
67
|
+
uploadBatch: (files: File[]) => Promise<NcpDraftAttachment[]>;
|
|
59
68
|
};
|
|
60
69
|
type ReadNcpDraftAttachmentsResult = {
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
attachments: NcpDraftAttachment[];
|
|
71
|
+
rejected: NcpRejectedAttachment[];
|
|
63
72
|
};
|
|
64
73
|
declare function buildNcpImageAttachmentDataUrl(attachment: NcpDraftAttachment): string;
|
|
65
74
|
declare function buildNcpRequestEnvelope(params: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
sessionId: string;
|
|
76
|
+
text?: string;
|
|
77
|
+
attachments?: readonly NcpDraftAttachment[];
|
|
78
|
+
parts?: readonly NcpMessagePart[];
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
80
|
+
messageId?: string;
|
|
81
|
+
timestamp?: string;
|
|
73
82
|
}): NcpRequestEnvelope | null;
|
|
74
83
|
declare function readFilesAsNcpDraftAttachments(files: Iterable<File>, options?: ReadNcpDraftAttachmentsOptions): Promise<ReadNcpDraftAttachmentsResult>;
|
|
75
84
|
declare function uploadFilesAsNcpDraftAttachments(files: Iterable<File>, options: UploadNcpDraftAttachmentsOptions): Promise<ReadNcpDraftAttachmentsResult>;
|
|
76
|
-
|
|
77
|
-
export { DEFAULT_NCP_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE, DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT, DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES, type NcpConversationSeed, type NcpConversationSeedLoader,
|
|
85
|
+
//#endregion
|
|
86
|
+
export { DEFAULT_NCP_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE, DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT, DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES, type NcpConversationSeed, type NcpConversationSeedLoader, NcpDraftAttachment, NcpRejectedAttachment, ReadNcpDraftAttachmentsOptions, ReadNcpDraftAttachmentsResult, UploadNcpDraftAttachmentsOptions, type UseHydratedNcpAgentOptions, type UseHydratedNcpAgentResult, type UseNcpAgentResult, buildNcpImageAttachmentDataUrl, buildNcpRequestEnvelope, readFilesAsNcpDraftAttachments, uploadFilesAsNcpDraftAttachments, useHydratedNcpAgent, useNcpAgent };
|
package/dist/index.js
CHANGED
|
@@ -1,478 +1,423 @@
|
|
|
1
|
-
|
|
2
|
-
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
3
|
-
|
|
4
|
-
// src/hooks/use-ncp-agent-runtime.ts
|
|
5
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
6
2
|
import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var EVENT_BATCH_DELAY_MS = 16;
|
|
3
|
+
import { NcpEventType } from "@nextclaw/ncp";
|
|
4
|
+
//#region src/hooks/use-ncp-agent-runtime.ts
|
|
5
|
+
const EVENT_BATCH_DELAY_MS = 16;
|
|
11
6
|
var NcpEventDispatchBatcher = class {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} finally {
|
|
53
|
-
this.isFlushing = false;
|
|
54
|
-
this.scheduleFlush();
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
7
|
+
constructor(dispatchBatch) {
|
|
8
|
+
this.dispatchBatch = dispatchBatch;
|
|
9
|
+
this.queue = [];
|
|
10
|
+
this.flushTimerId = null;
|
|
11
|
+
this.isFlushing = false;
|
|
12
|
+
this.isDisposed = false;
|
|
13
|
+
this.enqueue = (event) => {
|
|
14
|
+
if (this.isDisposed) return;
|
|
15
|
+
this.queue.push(event);
|
|
16
|
+
this.scheduleFlush();
|
|
17
|
+
};
|
|
18
|
+
this.dispose = () => {
|
|
19
|
+
this.isDisposed = true;
|
|
20
|
+
if (this.flushTimerId !== null) {
|
|
21
|
+
window.clearTimeout(this.flushTimerId);
|
|
22
|
+
this.flushTimerId = null;
|
|
23
|
+
}
|
|
24
|
+
this.queue.length = 0;
|
|
25
|
+
};
|
|
26
|
+
this.scheduleFlush = () => {
|
|
27
|
+
if (this.flushTimerId !== null || this.isFlushing || this.queue.length === 0) return;
|
|
28
|
+
this.flushTimerId = window.setTimeout(() => {
|
|
29
|
+
this.flushTimerId = null;
|
|
30
|
+
this.flush();
|
|
31
|
+
}, EVENT_BATCH_DELAY_MS);
|
|
32
|
+
};
|
|
33
|
+
this.flush = async () => {
|
|
34
|
+
if (this.isDisposed || this.isFlushing || this.queue.length === 0) return;
|
|
35
|
+
this.isFlushing = true;
|
|
36
|
+
try {
|
|
37
|
+
while (this.queue.length > 0) {
|
|
38
|
+
const batch = this.queue.splice(0);
|
|
39
|
+
await this.dispatchBatch(batch);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
this.isFlushing = false;
|
|
43
|
+
this.scheduleFlush();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
58
47
|
};
|
|
59
48
|
function dispatchEventsToManager(manager, events) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return events.reduce(
|
|
65
|
-
(chain, event) => chain.then(() => manager.dispatch(event)),
|
|
66
|
-
Promise.resolve()
|
|
67
|
-
);
|
|
49
|
+
const batchDispatch = manager.dispatchBatch;
|
|
50
|
+
if (typeof batchDispatch === "function") return batchDispatch.call(manager, events);
|
|
51
|
+
return events.reduce((chain, event) => chain.then(() => manager.dispatch(event)), Promise.resolve());
|
|
68
52
|
}
|
|
69
53
|
function shouldDispatchEventToSession(event, sessionId) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!("sessionId" in payload) || typeof payload.sessionId !== "string") {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return payload.sessionId === sessionId;
|
|
54
|
+
const payload = "payload" in event ? event.payload : null;
|
|
55
|
+
if (!payload || typeof payload !== "object") return true;
|
|
56
|
+
if (!("sessionId" in payload) || typeof payload.sessionId !== "string") return true;
|
|
57
|
+
return payload.sessionId === sessionId;
|
|
78
58
|
}
|
|
79
59
|
function hasMessageContent(message) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return true;
|
|
85
|
-
});
|
|
60
|
+
return message.parts.some((part) => {
|
|
61
|
+
if (part.type === "text" || part.type === "rich-text" || part.type === "reasoning") return part.text.trim().length > 0;
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
86
64
|
}
|
|
87
65
|
function normalizeSendEnvelope(input, sessionId) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
};
|
|
66
|
+
if (typeof input === "string") {
|
|
67
|
+
const content = input.trim();
|
|
68
|
+
if (!content) return null;
|
|
69
|
+
return {
|
|
70
|
+
sessionId,
|
|
71
|
+
message: {
|
|
72
|
+
id: `user-${Date.now().toString(36)}`,
|
|
73
|
+
sessionId,
|
|
74
|
+
role: "user",
|
|
75
|
+
status: "final",
|
|
76
|
+
parts: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: content
|
|
79
|
+
}],
|
|
80
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!hasMessageContent(input.message)) return null;
|
|
85
|
+
return {
|
|
86
|
+
...input,
|
|
87
|
+
sessionId: input.sessionId,
|
|
88
|
+
message: {
|
|
89
|
+
...input.message,
|
|
90
|
+
sessionId: input.message.sessionId || input.sessionId
|
|
91
|
+
}
|
|
92
|
+
};
|
|
116
93
|
}
|
|
117
94
|
function useScopedAgentManager(sessionId) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
return managerRef.current.manager;
|
|
95
|
+
const managerRef = useRef();
|
|
96
|
+
if (!managerRef.current || managerRef.current.sessionId !== sessionId) managerRef.current = {
|
|
97
|
+
sessionId,
|
|
98
|
+
manager: new DefaultNcpAgentConversationStateManager()
|
|
99
|
+
};
|
|
100
|
+
return managerRef.current.manager;
|
|
126
101
|
}
|
|
127
|
-
function useNcpAgentRuntime({
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const streamRun = async () => {
|
|
190
|
-
await client.stop();
|
|
191
|
-
await client.stream({ sessionId });
|
|
192
|
-
};
|
|
193
|
-
return {
|
|
194
|
-
snapshot,
|
|
195
|
-
visibleMessages,
|
|
196
|
-
activeRunId,
|
|
197
|
-
isRunning,
|
|
198
|
-
isSending,
|
|
199
|
-
send,
|
|
200
|
-
abort,
|
|
201
|
-
streamRun
|
|
202
|
-
};
|
|
102
|
+
function useNcpAgentRuntime({ sessionId, client, manager }) {
|
|
103
|
+
const snapshot = useSyncExternalStore((onStoreChange) => manager.subscribe(() => onStoreChange()), () => manager.getSnapshot(), () => manager.getSnapshot());
|
|
104
|
+
const [isSending, setIsSending] = useState(false);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
setIsSending(false);
|
|
107
|
+
}, [sessionId]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const eventBatcher = new NcpEventDispatchBatcher((events) => dispatchEventsToManager(manager, events));
|
|
110
|
+
const unsubscribeClient = client.subscribe((event) => {
|
|
111
|
+
if (!shouldDispatchEventToSession(event, sessionId)) return;
|
|
112
|
+
eventBatcher.enqueue(event);
|
|
113
|
+
});
|
|
114
|
+
return () => {
|
|
115
|
+
unsubscribeClient();
|
|
116
|
+
eventBatcher.dispose();
|
|
117
|
+
client.stop();
|
|
118
|
+
};
|
|
119
|
+
}, [
|
|
120
|
+
client,
|
|
121
|
+
manager,
|
|
122
|
+
sessionId
|
|
123
|
+
]);
|
|
124
|
+
const visibleMessages = snapshot.streamingMessage ? [...snapshot.messages, snapshot.streamingMessage] : snapshot.messages;
|
|
125
|
+
const activeRunId = snapshot.activeRun?.runId ?? null;
|
|
126
|
+
const isRunning = !!snapshot.activeRun;
|
|
127
|
+
const send = async (input) => {
|
|
128
|
+
if (isSending || isRunning) return;
|
|
129
|
+
const envelope = normalizeSendEnvelope(input, sessionId);
|
|
130
|
+
if (!envelope) return;
|
|
131
|
+
setIsSending(true);
|
|
132
|
+
await manager.dispatch({
|
|
133
|
+
type: NcpEventType.MessageSent,
|
|
134
|
+
payload: {
|
|
135
|
+
sessionId,
|
|
136
|
+
message: envelope.message,
|
|
137
|
+
metadata: envelope.metadata
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await client.send(envelope);
|
|
142
|
+
} finally {
|
|
143
|
+
setIsSending(false);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const abort = async () => {
|
|
147
|
+
if (!snapshot.activeRun) return;
|
|
148
|
+
await client.abort({ sessionId });
|
|
149
|
+
};
|
|
150
|
+
const streamRun = async () => {
|
|
151
|
+
await client.stop();
|
|
152
|
+
await client.stream({ sessionId });
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
snapshot,
|
|
156
|
+
visibleMessages,
|
|
157
|
+
activeRunId,
|
|
158
|
+
isRunning,
|
|
159
|
+
isSending,
|
|
160
|
+
send,
|
|
161
|
+
abort,
|
|
162
|
+
streamRun
|
|
163
|
+
};
|
|
203
164
|
}
|
|
204
|
-
|
|
205
|
-
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/hooks/use-hydrated-ncp-agent.ts
|
|
206
167
|
function toError(error) {
|
|
207
|
-
|
|
168
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
208
169
|
}
|
|
209
170
|
function resolveSessionHydratingState(params) {
|
|
210
|
-
|
|
171
|
+
return params.isHydrating || params.hydratedSessionId !== params.sessionId;
|
|
211
172
|
}
|
|
212
|
-
function useHydratedNcpAgent({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
hydrateError
|
|
288
|
-
};
|
|
173
|
+
function useHydratedNcpAgent({ sessionId, client, loadSeed }) {
|
|
174
|
+
const manager = useScopedAgentManager(sessionId);
|
|
175
|
+
const runtime = useNcpAgentRuntime({
|
|
176
|
+
sessionId,
|
|
177
|
+
client,
|
|
178
|
+
manager
|
|
179
|
+
});
|
|
180
|
+
const [isHydrating, setIsHydrating] = useState(true);
|
|
181
|
+
const [hydrateError, setHydrateError] = useState(null);
|
|
182
|
+
const [hydratedSessionId, setHydratedSessionId] = useState(null);
|
|
183
|
+
const loadStateRef = useRef({
|
|
184
|
+
requestId: 0,
|
|
185
|
+
controller: null
|
|
186
|
+
});
|
|
187
|
+
const hydrateSeed = useCallback(async () => {
|
|
188
|
+
loadStateRef.current.controller?.abort();
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const requestId = loadStateRef.current.requestId + 1;
|
|
191
|
+
loadStateRef.current = {
|
|
192
|
+
requestId,
|
|
193
|
+
controller
|
|
194
|
+
};
|
|
195
|
+
await client.stop();
|
|
196
|
+
manager.reset();
|
|
197
|
+
setHydrateError(null);
|
|
198
|
+
setIsHydrating(true);
|
|
199
|
+
try {
|
|
200
|
+
const seed = await loadSeed(sessionId, controller.signal);
|
|
201
|
+
if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) return;
|
|
202
|
+
manager.hydrate({
|
|
203
|
+
sessionId,
|
|
204
|
+
messages: seed.messages,
|
|
205
|
+
activeRun: seed.status === "running" ? {
|
|
206
|
+
runId: null,
|
|
207
|
+
sessionId,
|
|
208
|
+
abortDisabledReason: null
|
|
209
|
+
} : null
|
|
210
|
+
});
|
|
211
|
+
setHydrateError(null);
|
|
212
|
+
setHydratedSessionId(sessionId);
|
|
213
|
+
setIsHydrating(false);
|
|
214
|
+
client.stream({ sessionId }).catch((error) => {
|
|
215
|
+
if (loadStateRef.current.requestId !== requestId) return;
|
|
216
|
+
setHydrateError(toError(error));
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) return;
|
|
220
|
+
setHydrateError(toError(error));
|
|
221
|
+
setHydratedSessionId(sessionId);
|
|
222
|
+
setIsHydrating(false);
|
|
223
|
+
} finally {
|
|
224
|
+
if (loadStateRef.current.controller === controller) loadStateRef.current.controller = null;
|
|
225
|
+
}
|
|
226
|
+
}, [
|
|
227
|
+
client,
|
|
228
|
+
loadSeed,
|
|
229
|
+
manager,
|
|
230
|
+
sessionId
|
|
231
|
+
]);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
hydrateSeed();
|
|
234
|
+
return () => {
|
|
235
|
+
loadStateRef.current.controller?.abort();
|
|
236
|
+
loadStateRef.current.controller = null;
|
|
237
|
+
};
|
|
238
|
+
}, [hydrateSeed]);
|
|
239
|
+
return {
|
|
240
|
+
...runtime,
|
|
241
|
+
isHydrating: resolveSessionHydratingState({
|
|
242
|
+
sessionId,
|
|
243
|
+
hydratedSessionId,
|
|
244
|
+
isHydrating
|
|
245
|
+
}),
|
|
246
|
+
hydrateError
|
|
247
|
+
};
|
|
289
248
|
}
|
|
290
|
-
|
|
291
|
-
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/hooks/use-ncp-agent.ts
|
|
292
251
|
function useNcpAgent(sessionId, client) {
|
|
293
|
-
|
|
294
|
-
|
|
252
|
+
return useNcpAgentRuntime({
|
|
253
|
+
sessionId,
|
|
254
|
+
client,
|
|
255
|
+
manager: useScopedAgentManager(sessionId)
|
|
256
|
+
});
|
|
295
257
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/attachments/ncp-attachments.ts
|
|
260
|
+
const DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT = "image/png,image/jpeg,image/webp,image/gif";
|
|
261
|
+
const DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES = [
|
|
262
|
+
"image/png",
|
|
263
|
+
"image/jpeg",
|
|
264
|
+
"image/webp",
|
|
265
|
+
"image/gif"
|
|
304
266
|
];
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
267
|
+
const DEFAULT_NCP_ATTACHMENT_MAX_BYTES = 200 * 1024 * 1024;
|
|
268
|
+
const DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES = DEFAULT_NCP_ATTACHMENT_MAX_BYTES;
|
|
269
|
+
const DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE = "application/octet-stream";
|
|
308
270
|
function createAttachmentId() {
|
|
309
|
-
|
|
271
|
+
return `ncp-file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
310
272
|
}
|
|
311
273
|
function readFileAsDataUrl(file) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const reader = new FileReader();
|
|
276
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error(`Failed to read ${file.name}`));
|
|
277
|
+
reader.onload = () => {
|
|
278
|
+
if (typeof reader.result !== "string") {
|
|
279
|
+
reject(/* @__PURE__ */ new Error(`Unexpected FileReader result for ${file.name}`));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
resolve(reader.result);
|
|
283
|
+
};
|
|
284
|
+
reader.readAsDataURL(file);
|
|
285
|
+
});
|
|
324
286
|
}
|
|
325
287
|
function toBase64Content(dataUrl) {
|
|
326
|
-
|
|
327
|
-
|
|
288
|
+
const commaIndex = dataUrl.indexOf(",");
|
|
289
|
+
return commaIndex >= 0 ? dataUrl.slice(commaIndex + 1) : dataUrl;
|
|
328
290
|
}
|
|
329
291
|
function normalizeAttachmentMimeType(file) {
|
|
330
|
-
|
|
331
|
-
|
|
292
|
+
const mimeType = file.type.trim().toLowerCase();
|
|
293
|
+
return mimeType.length > 0 ? mimeType : DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE;
|
|
332
294
|
}
|
|
333
295
|
function validateAttachmentFile(file, options) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return { ok: true, mimeType };
|
|
296
|
+
const acceptedMimeTypes = options.acceptedMimeTypes && options.acceptedMimeTypes.length > 0 ? new Set(options.acceptedMimeTypes.map((mimeType) => mimeType.trim().toLowerCase())) : null;
|
|
297
|
+
const maxBytes = options.maxBytes ?? 209715200;
|
|
298
|
+
const mimeType = normalizeAttachmentMimeType(file);
|
|
299
|
+
if (acceptedMimeTypes && !acceptedMimeTypes.has(mimeType)) return {
|
|
300
|
+
ok: false,
|
|
301
|
+
rejected: {
|
|
302
|
+
fileName: file.name,
|
|
303
|
+
mimeType,
|
|
304
|
+
sizeBytes: file.size,
|
|
305
|
+
reason: "unsupported-type"
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
if (file.size > maxBytes) return {
|
|
309
|
+
ok: false,
|
|
310
|
+
rejected: {
|
|
311
|
+
fileName: file.name,
|
|
312
|
+
mimeType,
|
|
313
|
+
sizeBytes: file.size,
|
|
314
|
+
reason: "too-large"
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
mimeType
|
|
320
|
+
};
|
|
360
321
|
}
|
|
361
322
|
function buildNcpImageAttachmentDataUrl(attachment) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!attachment.contentBase64?.trim()) {
|
|
366
|
-
throw new Error(`Attachment ${attachment.name} does not have image content.`);
|
|
367
|
-
}
|
|
368
|
-
return `data:${attachment.mimeType};base64,${attachment.contentBase64}`;
|
|
323
|
+
if (attachment.url?.trim()) return attachment.url.trim();
|
|
324
|
+
if (!attachment.contentBase64?.trim()) throw new Error(`Attachment ${attachment.name} does not have image content.`);
|
|
325
|
+
return `data:${attachment.mimeType};base64,${attachment.contentBase64}`;
|
|
369
326
|
}
|
|
370
327
|
function buildNcpRequestEnvelope(params) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
...params.metadata ? { metadata: params.metadata } : {}
|
|
400
|
-
};
|
|
328
|
+
const parts = params.parts && params.parts.length > 0 ? params.parts.map((part) => structuredClone(part)) : [...params.text?.trim() ?? "" ? [{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: params.text.trim()
|
|
331
|
+
}] : [], ...(params.attachments ?? []).map((attachment) => ({
|
|
332
|
+
type: "file",
|
|
333
|
+
name: attachment.name,
|
|
334
|
+
mimeType: attachment.mimeType,
|
|
335
|
+
...attachment.assetUri?.trim() ? { assetUri: attachment.assetUri.trim() } : {},
|
|
336
|
+
...attachment.url?.trim() ? { url: attachment.url.trim() } : {},
|
|
337
|
+
...attachment.contentBase64?.trim() ? { contentBase64: attachment.contentBase64.trim() } : {},
|
|
338
|
+
sizeBytes: attachment.sizeBytes
|
|
339
|
+
}))];
|
|
340
|
+
if (parts.length === 0) return null;
|
|
341
|
+
const timestamp = params.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
342
|
+
const messageId = params.messageId ?? `user-${Date.now().toString(36)}`;
|
|
343
|
+
return {
|
|
344
|
+
sessionId: params.sessionId,
|
|
345
|
+
message: {
|
|
346
|
+
id: messageId,
|
|
347
|
+
sessionId: params.sessionId,
|
|
348
|
+
role: "user",
|
|
349
|
+
status: "final",
|
|
350
|
+
parts,
|
|
351
|
+
timestamp,
|
|
352
|
+
...params.metadata ? { metadata: params.metadata } : {}
|
|
353
|
+
},
|
|
354
|
+
...params.metadata ? { metadata: params.metadata } : {}
|
|
355
|
+
};
|
|
401
356
|
}
|
|
402
357
|
async function readFilesAsNcpDraftAttachments(files, options = {}) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
358
|
+
const attachments = [];
|
|
359
|
+
const rejected = [];
|
|
360
|
+
for (const file of files) {
|
|
361
|
+
const validation = validateAttachmentFile(file, options);
|
|
362
|
+
if (!validation.ok) {
|
|
363
|
+
rejected.push(validation.rejected);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
368
|
+
attachments.push({
|
|
369
|
+
id: createAttachmentId(),
|
|
370
|
+
name: file.name,
|
|
371
|
+
mimeType: validation.mimeType,
|
|
372
|
+
contentBase64: toBase64Content(dataUrl),
|
|
373
|
+
sizeBytes: file.size
|
|
374
|
+
});
|
|
375
|
+
} catch {
|
|
376
|
+
rejected.push({
|
|
377
|
+
fileName: file.name,
|
|
378
|
+
mimeType: validation.mimeType,
|
|
379
|
+
sizeBytes: file.size,
|
|
380
|
+
reason: "read-failed"
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
attachments,
|
|
386
|
+
rejected
|
|
387
|
+
};
|
|
430
388
|
}
|
|
431
389
|
async function uploadFilesAsNcpDraftAttachments(files, options) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
};
|
|
464
|
-
}
|
|
390
|
+
const validFiles = [];
|
|
391
|
+
const rejected = [];
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
const validation = validateAttachmentFile(file, options);
|
|
394
|
+
if (!validation.ok) {
|
|
395
|
+
rejected.push(validation.rejected);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
validFiles.push(file);
|
|
399
|
+
}
|
|
400
|
+
if (validFiles.length === 0) return {
|
|
401
|
+
attachments: [],
|
|
402
|
+
rejected
|
|
403
|
+
};
|
|
404
|
+
try {
|
|
405
|
+
return {
|
|
406
|
+
attachments: await options.uploadBatch(validFiles),
|
|
407
|
+
rejected
|
|
408
|
+
};
|
|
409
|
+
} catch {
|
|
410
|
+
rejected.push(...validFiles.map((file) => ({
|
|
411
|
+
fileName: file.name,
|
|
412
|
+
mimeType: normalizeAttachmentMimeType(file),
|
|
413
|
+
sizeBytes: file.size,
|
|
414
|
+
reason: "read-failed"
|
|
415
|
+
})));
|
|
416
|
+
return {
|
|
417
|
+
attachments: [],
|
|
418
|
+
rejected
|
|
419
|
+
};
|
|
420
|
+
}
|
|
465
421
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE,
|
|
469
|
-
DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT,
|
|
470
|
-
DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES,
|
|
471
|
-
DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES,
|
|
472
|
-
buildNcpImageAttachmentDataUrl,
|
|
473
|
-
buildNcpRequestEnvelope,
|
|
474
|
-
readFilesAsNcpDraftAttachments,
|
|
475
|
-
uploadFilesAsNcpDraftAttachments,
|
|
476
|
-
useHydratedNcpAgent,
|
|
477
|
-
useNcpAgent
|
|
478
|
-
};
|
|
422
|
+
//#endregion
|
|
423
|
+
export { DEFAULT_NCP_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_FALLBACK_ATTACHMENT_MIME_TYPE, DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT, DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES, DEFAULT_NCP_IMAGE_ATTACHMENT_MIME_TYPES, buildNcpImageAttachmentDataUrl, buildNcpRequestEnvelope, readFilesAsNcpDraftAttachments, uploadFilesAsNcpDraftAttachments, useHydratedNcpAgent, useNcpAgent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React bindings for building NCP-based agent applications.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@nextclaw/ncp": "0.5.0",
|
|
19
|
-
"@nextclaw/ncp-toolkit": "0.5.
|
|
19
|
+
"@nextclaw/ncp-toolkit": "0.5.1"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -25,11 +25,10 @@
|
|
|
25
25
|
"@types/node": "^20.17.6",
|
|
26
26
|
"@types/react": "^18.3.12",
|
|
27
27
|
"prettier": "^3.3.3",
|
|
28
|
-
"tsup": "^8.3.5",
|
|
29
28
|
"typescript": "^5.6.3"
|
|
30
29
|
},
|
|
31
30
|
"scripts": {
|
|
32
|
-
"build": "
|
|
31
|
+
"build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension",
|
|
33
32
|
"lint": "cd ../../.. && pnpm exec eslint packages/ncp-packages/nextclaw-ncp-react --config packages/ncp-packages/nextclaw-ncp-react/eslint.config.mjs",
|
|
34
33
|
"tsc": "tsc -p tsconfig.json"
|
|
35
34
|
}
|