@nextclaw/ncp-react 0.4.15 → 0.4.17

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