@blocksdiy/react-common 1.50.1 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/new-agent-chat.d.ts +14 -0
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +204 -80
- package/dist/hooks/agent-chat-own-runs.d.ts +3 -0
- package/dist/hooks/agent-chat-own-runs.d.ts.map +1 -0
- package/dist/hooks/agent-chat-own-runs.js +8 -0
- package/dist/hooks/use-agent-conversation.d.ts +7 -1
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +27 -3
- package/dist/hooks/use-agent-threads.d.ts +1 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +1 -0
- package/dist/utils/agent-admin.d.ts +28 -0
- package/dist/utils/agent-admin.d.ts.map +1 -0
- package/dist/utils/agent-admin.js +48 -0
- package/dist/utils/agui-interrupts.d.ts +179 -0
- package/dist/utils/agui-interrupts.d.ts.map +1 -0
- package/dist/utils/agui-interrupts.js +351 -0
- package/package.json +2 -2
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
export const TOOL_APPROVALS_METADATA_KEY = "blocks.toolApprovals";
|
|
2
|
+
const SUBAGENT_TOOL_NAME = "task";
|
|
3
|
+
/**
|
|
4
|
+
* A rejected call never ran; its result is the rejection. The server always uses
|
|
5
|
+
* HumanInTheLoopMiddleware's standard reject (the Reject button, typing, and a new
|
|
6
|
+
* Send Message). Used only when no record of the decision exists: a
|
|
7
|
+
* tool that failed also carries `error`.
|
|
8
|
+
*/
|
|
9
|
+
const REJECTED_RESULT_PREFIXES = ["User rejected the tool call for `"];
|
|
10
|
+
export const APPROVAL_STATUS_LINE = {
|
|
11
|
+
waiting: "Waiting for approval",
|
|
12
|
+
approved: "Approved",
|
|
13
|
+
rejected: "Rejected",
|
|
14
|
+
};
|
|
15
|
+
/** Appended to a decided card's status while another request of the pause is open. */
|
|
16
|
+
export const WAITING_FOR_OTHER_SUFFIX = " · waiting for your decision on the other request";
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
const isString = (value) => typeof value === "string";
|
|
19
|
+
const nonEmpty = (value) => (isString(value) && value ? value : null);
|
|
20
|
+
const parseArgs = (raw) => {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw || "{}");
|
|
23
|
+
return isRecord(parsed) ? parsed : {};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const requestKey = (interruptId, index) => `${interruptId}#${index}`;
|
|
30
|
+
/** The name the model sees for a describe name (wf-actions-py `model_tool_name`). */
|
|
31
|
+
const modelToolName = (describeName) => describeName.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
|
32
|
+
/**
|
|
33
|
+
* The server arms each approval with its tool's describe name as the request's
|
|
34
|
+
* `description`. Trusted only when it maps back to the request's name: a pause armed
|
|
35
|
+
* without one (and every pause parked before) carries the middleware's default text.
|
|
36
|
+
*/
|
|
37
|
+
const displayNameOf = (name, description) => isString(description) && description && name && modelToolName(description) === name ? description : null;
|
|
38
|
+
const rawOf = (interrupt) => {
|
|
39
|
+
const metadata = isRecord(interrupt.metadata) ? interrupt.metadata : {};
|
|
40
|
+
const langgraph = isRecord(metadata.langgraph) ? metadata.langgraph : {};
|
|
41
|
+
return isRecord(langgraph.raw) ? langgraph.raw : {};
|
|
42
|
+
};
|
|
43
|
+
const allowedOf = (configs, index, name) => {
|
|
44
|
+
const read = (config) => isRecord(config) && Array.isArray(config.allowed_decisions) ? config.allowed_decisions.filter(isString) : [];
|
|
45
|
+
// The middleware appends one request and one config per gated call, in step.
|
|
46
|
+
const positional = configs[index];
|
|
47
|
+
if (isRecord(positional) && (!name || positional.action_name == null || positional.action_name === name)) {
|
|
48
|
+
return read(positional);
|
|
49
|
+
}
|
|
50
|
+
const byName = configs.find((config) => isRecord(config) && config.action_name === name);
|
|
51
|
+
return byName ? read(byName) : read(positional);
|
|
52
|
+
};
|
|
53
|
+
/** The newest assistant message's calls that have no result yet, in order. */
|
|
54
|
+
const waitingCalls = (messages) => {
|
|
55
|
+
const answered = new Set();
|
|
56
|
+
for (const message of messages) {
|
|
57
|
+
if (message.role === "tool" && "toolCallId" in message && isString(message.toolCallId)) {
|
|
58
|
+
answered.add(message.toolCallId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
62
|
+
const message = messages[i];
|
|
63
|
+
if (message.role !== "assistant") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const calls = "toolCalls" in message && Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
67
|
+
return calls
|
|
68
|
+
.filter((call) => !answered.has(call.id))
|
|
69
|
+
.map((call) => ({ id: call.id, name: call.function.name, args: parseArgs(call.function.arguments) }));
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Every request of the open interrupts, in the order the server needs decisions,
|
|
75
|
+
* each paired with its transcript call by order.
|
|
76
|
+
*/
|
|
77
|
+
export const requestsOf = (interrupts, messages) => {
|
|
78
|
+
const waiting = waitingCalls(messages);
|
|
79
|
+
const tasks = waiting.filter((call) => call.name === SUBAGENT_TOOL_NAME);
|
|
80
|
+
const own = waiting.filter((call) => call.name !== SUBAGENT_TOOL_NAME);
|
|
81
|
+
const host = tasks[0] ?? null;
|
|
82
|
+
const used = new Set();
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const interrupt of interrupts) {
|
|
85
|
+
const raw = rawOf(interrupt);
|
|
86
|
+
const requests = Array.isArray(raw.action_requests) ? raw.action_requests : [];
|
|
87
|
+
const configs = Array.isArray(raw.review_configs) ? raw.review_configs : [];
|
|
88
|
+
requests.forEach((request, index) => {
|
|
89
|
+
const fields = isRecord(request) ? request : {};
|
|
90
|
+
const name = isString(fields.name) ? fields.name : "";
|
|
91
|
+
const match = own.find((call) => call.name === name && !used.has(call.id)) ?? null;
|
|
92
|
+
if (match) {
|
|
93
|
+
used.add(match.id);
|
|
94
|
+
}
|
|
95
|
+
out.push({
|
|
96
|
+
interruptId: interrupt.id,
|
|
97
|
+
index,
|
|
98
|
+
key: requestKey(interrupt.id, index),
|
|
99
|
+
name,
|
|
100
|
+
args: isRecord(fields.args) ? fields.args : {},
|
|
101
|
+
allowedDecisions: allowedOf(configs, index, name),
|
|
102
|
+
toolCallId: match?.id ?? null,
|
|
103
|
+
anchorToolCallId: match ? match.id : (host?.id ?? null),
|
|
104
|
+
displayName: displayNameOf(name, fields.description),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
};
|
|
110
|
+
export const isApprovalRequest = (request) => request.allowedDecisions.includes("approve");
|
|
111
|
+
/** Thrown with fixed text when an answer pairs with no open interrupt and is not the welcome seed. */
|
|
112
|
+
export const INTERRUPT_NOT_OPEN_ERROR = "answerInterrupt: nothing is waiting for this answer";
|
|
113
|
+
/**
|
|
114
|
+
* A question's answer as HumanInTheLoopMiddleware's `respond` message -- and as the welcome
|
|
115
|
+
* seed's tool result, so both read the same. `?? null`: JSON.stringify(undefined) drops the
|
|
116
|
+
* key, which the server refuses.
|
|
117
|
+
*/
|
|
118
|
+
export const respondMessageOf = (answer) => JSON.stringify(answer ?? null);
|
|
119
|
+
const decisionOf = (answer) => answer.type === "respond" ? { type: "respond", message: respondMessageOf(answer.message) } : { type: answer.type };
|
|
120
|
+
/**
|
|
121
|
+
* The welcome seed (`initialUserChoices`): connect writes an ask_user call that no run made,
|
|
122
|
+
* so nothing is parked behind it. Its signature is the one the server used
|
|
123
|
+
* (`answer_orphan_pause`): no user message yet, and the call still waits on the newest
|
|
124
|
+
* assistant message.
|
|
125
|
+
*/
|
|
126
|
+
export const isWelcomeSeedCall = (messages, toolCallId) => !messages.some((message) => message.role === "user") && waitingCalls(messages).some((call) => call.id === toolCallId);
|
|
127
|
+
/**
|
|
128
|
+
* Where a question card's answer goes: the open request it pairs with by order; the seed's
|
|
129
|
+
* tool result when nothing is open; otherwise nowhere -- the caller
|
|
130
|
+
* throws INTERRUPT_NOT_OPEN_ERROR. Never `command.resume`.
|
|
131
|
+
*/
|
|
132
|
+
export const answerRouteOf = (interrupts, messages, toolCallId) => {
|
|
133
|
+
const request = requestsOf(interrupts, messages).find((candidate) => candidate.toolCallId === toolCallId && !isApprovalRequest(candidate));
|
|
134
|
+
if (request) {
|
|
135
|
+
return { kind: "respond", request };
|
|
136
|
+
}
|
|
137
|
+
if (interrupts.length === 0 && isWelcomeSeedCall(messages, toolCallId)) {
|
|
138
|
+
return { kind: "seed" };
|
|
139
|
+
}
|
|
140
|
+
return { kind: "refuse" };
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The deprecated `resolveInterrupt(resume)`: a `{decisions}` payload for the ONE
|
|
144
|
+
* open interrupt, as a standard resolved entry. Null when it cannot be routed.
|
|
145
|
+
*/
|
|
146
|
+
export const singleInterruptResume = (interrupts, payload) => interrupts.length === 1 && isRecord(payload) && Array.isArray(payload.decisions)
|
|
147
|
+
? [{ interruptId: interrupts[0].id, status: "resolved", payload: { decisions: payload.decisions } }]
|
|
148
|
+
: null;
|
|
149
|
+
/**
|
|
150
|
+
* After a run that carried `resume[]`: anything but RUN_FINISHED (a RUN_ERROR such as a refused
|
|
151
|
+
* resume, a stop, a dropped stream) may have consumed the resume on the server, while
|
|
152
|
+
* @ag-ui/client changes `pendingInterrupts` only on RUN_FINISHED. The chat then forgets them and
|
|
153
|
+
* reconnects; the connect re-sends what is really open.
|
|
154
|
+
*/
|
|
155
|
+
export const needsReconnectAfter = (sentResume, finished) => sentResume && !finished;
|
|
156
|
+
/** One `resolved` entry per open interrupt, or null while any request is unanswered. */
|
|
157
|
+
export const buildResumeEntries = (interrupts, requests, answers) => {
|
|
158
|
+
if (interrupts.length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const interrupt of interrupts) {
|
|
163
|
+
const decisions = [];
|
|
164
|
+
for (const request of requests.filter((r) => r.interruptId === interrupt.id)) {
|
|
165
|
+
const answer = answers.get(request.key);
|
|
166
|
+
if (!answer) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
decisions.push(decisionOf(answer));
|
|
170
|
+
}
|
|
171
|
+
entries.push({ interruptId: interrupt.id, status: "resolved", payload: { decisions } });
|
|
172
|
+
}
|
|
173
|
+
return entries;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Typing while the turn is parked: the typed text goes out as a normal
|
|
177
|
+
* message, and each open interrupt gets the protocol's own `cancelled` entry, with no payload.
|
|
178
|
+
* The server rejects every waiting approval with the standard text, skips every question with
|
|
179
|
+
* one fixed marker, and appends the message after them.
|
|
180
|
+
*/
|
|
181
|
+
export const cancelledResumeEntries = (interrupts) => interrupts.map((interrupt) => ({ interruptId: interrupt.id, status: "cancelled" }));
|
|
182
|
+
/**
|
|
183
|
+
* Forgets the agent's open interrupts. CopilotKit 1.70.2 keeps
|
|
184
|
+
* `pendingInterrupts` across a thread swap and a reconnect, and @ag-ui/client 0.0.59
|
|
185
|
+
* then refuses the next run AND connect ("Thread has N pending interrupt(s) not
|
|
186
|
+
* addressed by resume"). Call it on a thread swap and right before every connect
|
|
187
|
+
* the chat starts; the connect's own RUN_FINISHED re-sends what is still open.
|
|
188
|
+
*/
|
|
189
|
+
export const forgetOpenInterrupts = (agent) => {
|
|
190
|
+
if (agent) {
|
|
191
|
+
agent.pendingInterrupts = [];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
/** This browser's own approval decisions, by anchor, until a reload brings the record. */
|
|
195
|
+
export const localStampsByAnchor = (requests, answers) => {
|
|
196
|
+
const out = new Map();
|
|
197
|
+
for (const request of requests) {
|
|
198
|
+
const answer = answers.get(request.key);
|
|
199
|
+
if (!answer || answer.type === "respond" || !isApprovalRequest(request) || !request.anchorToolCallId) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const list = out.get(request.anchorToolCallId) ?? [];
|
|
203
|
+
list.push({
|
|
204
|
+
// A sub-agent's call is not in this transcript; its request key stands in.
|
|
205
|
+
toolCallId: request.toolCallId ?? request.key,
|
|
206
|
+
decision: answer.type,
|
|
207
|
+
toolName: request.name,
|
|
208
|
+
displayName: request.displayName,
|
|
209
|
+
args: request.args,
|
|
210
|
+
});
|
|
211
|
+
out.set(request.anchorToolCallId, list);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
};
|
|
215
|
+
export const mergeStamps = (previous, next) => {
|
|
216
|
+
if (next.size === 0) {
|
|
217
|
+
return previous;
|
|
218
|
+
}
|
|
219
|
+
const merged = new Map(previous);
|
|
220
|
+
next.forEach((stamps, anchor) => merged.set(anchor, [...(merged.get(anchor) ?? []), ...stamps]));
|
|
221
|
+
return merged;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* The decided-here stamps after the chat forgets what is open. A same-thread reload keeps them:
|
|
225
|
+
* the connect's snapshot brings the record, and until it lands nothing else draws the card. A
|
|
226
|
+
* thread or chat swap, or a reconnect after a failed run, clears them.
|
|
227
|
+
*/
|
|
228
|
+
export const stampsAfterInterruptReset = (previous, { keepStamps }) => (keepStamps || previous.size === 0 ? previous : new Map());
|
|
229
|
+
/** Takes back exactly the stamps `mergeStamps` added (by identity), when their send did not go through. */
|
|
230
|
+
export const dropStamps = (previous, added) => {
|
|
231
|
+
if (added.size === 0) {
|
|
232
|
+
return previous;
|
|
233
|
+
}
|
|
234
|
+
const kept = new Map();
|
|
235
|
+
previous.forEach((stamps, anchor) => {
|
|
236
|
+
const drop = new Set(added.get(anchor) ?? []);
|
|
237
|
+
const rest = stamps.filter((stamp) => !drop.has(stamp));
|
|
238
|
+
if (rest.length > 0) {
|
|
239
|
+
kept.set(anchor, rest);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return kept;
|
|
243
|
+
};
|
|
244
|
+
export const toolApprovalsOf = (message) => {
|
|
245
|
+
if (!isRecord(message) || !isRecord(message.metadata)) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const raw = message.metadata[TOOL_APPROVALS_METADATA_KEY];
|
|
249
|
+
if (!Array.isArray(raw)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
return raw.flatMap((item) => isRecord(item) &&
|
|
253
|
+
(item.decision === "approve" || item.decision === "reject") &&
|
|
254
|
+
isString(item.toolName) &&
|
|
255
|
+
isString(item.toolCallId)
|
|
256
|
+
? [
|
|
257
|
+
{
|
|
258
|
+
toolCallId: item.toolCallId,
|
|
259
|
+
decision: item.decision,
|
|
260
|
+
toolName: item.toolName,
|
|
261
|
+
displayName: nonEmpty(item.displayName),
|
|
262
|
+
args: isRecord(item.args) ? item.args : {},
|
|
263
|
+
},
|
|
264
|
+
]
|
|
265
|
+
: []);
|
|
266
|
+
};
|
|
267
|
+
const rejectedByResult = (result) => isString(result) && REJECTED_RESULT_PREFIXES.some((prefix) => result.startsWith(prefix));
|
|
268
|
+
/** `insert_items_in_orders` → `insert items in orders`. */
|
|
269
|
+
export const toolWords = (name) => name
|
|
270
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
271
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
272
|
+
.replace(/[_\s-]+/g, " ")
|
|
273
|
+
.trim()
|
|
274
|
+
.toLowerCase();
|
|
275
|
+
export const toolCallOf = (messages, toolCallId) => {
|
|
276
|
+
for (const message of messages) {
|
|
277
|
+
if (message.role !== "assistant" || !("toolCalls" in message) || !Array.isArray(message.toolCalls)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const call = message.toolCalls.find((candidate) => candidate.id === toolCallId);
|
|
281
|
+
if (call) {
|
|
282
|
+
return { name: call.function.name, args: parseArgs(call.function.arguments) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
};
|
|
287
|
+
const titleOf = (toolName) => {
|
|
288
|
+
const words = toolWords(toolName);
|
|
289
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
290
|
+
};
|
|
291
|
+
const statusOf = (decision) => decision === "approve" ? "approved" : "rejected";
|
|
292
|
+
export const approvalCardsForToolCall = ({ toolCallId, toolName, toolArgs, toolResult, toolMessage, requests, answers, localStamps, }) => {
|
|
293
|
+
const live = requests.filter((request) => request.anchorToolCallId === toolCallId && isApprovalRequest(request));
|
|
294
|
+
if (live.length > 0) {
|
|
295
|
+
const othersOpen = requests.some((request) => !answers.has(request.key));
|
|
296
|
+
return live.map((request) => {
|
|
297
|
+
const answer = answers.get(request.key);
|
|
298
|
+
const status = answer && answer.type !== "respond" ? statusOf(answer.type) : "waiting";
|
|
299
|
+
const waitingForOthers = status !== "waiting" && othersOpen;
|
|
300
|
+
return {
|
|
301
|
+
key: request.key,
|
|
302
|
+
title: titleOf(request.displayName ?? request.name),
|
|
303
|
+
args: request.args,
|
|
304
|
+
status,
|
|
305
|
+
statusLine: waitingForOthers
|
|
306
|
+
? `${APPROVAL_STATUS_LINE[status]}${WAITING_FOR_OTHER_SUFFIX}`
|
|
307
|
+
: APPROVAL_STATUS_LINE[status],
|
|
308
|
+
request: status === "waiting" ? { interruptId: request.interruptId, index: request.index } : null,
|
|
309
|
+
waitingForOthers,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// The server's record wins once a reload brought it; until then, this browser's own.
|
|
314
|
+
const recorded = toolApprovalsOf(toolMessage);
|
|
315
|
+
const decided = recorded.length > 0 ? recorded : (localStamps.get(toolCallId) ?? []);
|
|
316
|
+
if (decided.length > 0) {
|
|
317
|
+
return decided.map((stamp) => {
|
|
318
|
+
const status = statusOf(stamp.decision);
|
|
319
|
+
return {
|
|
320
|
+
key: `${toolCallId}#${stamp.toolCallId}`,
|
|
321
|
+
title: titleOf(stamp.displayName ?? stamp.toolName),
|
|
322
|
+
args: stamp.args,
|
|
323
|
+
status,
|
|
324
|
+
statusLine: APPROVAL_STATUS_LINE[status],
|
|
325
|
+
request: null,
|
|
326
|
+
waitingForOthers: false,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (toolName !== SUBAGENT_TOOL_NAME && rejectedByResult(toolResult)) {
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
key: `${toolCallId}#rejected`,
|
|
334
|
+
title: titleOf(toolName),
|
|
335
|
+
args: isRecord(toolArgs) ? toolArgs : {},
|
|
336
|
+
status: "rejected",
|
|
337
|
+
statusLine: APPROVAL_STATUS_LINE.rejected,
|
|
338
|
+
request: null,
|
|
339
|
+
waitingForOthers: false,
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
return [];
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Whether the tool's normal step renders under its cards: a sub-agent's `task` step
|
|
347
|
+
* always does; a parent call's only once approved AND sent (while waiting it has not
|
|
348
|
+
* run; a rejected call never runs; an approval waiting for the pause's other cards has
|
|
349
|
+
* not been sent yet).
|
|
350
|
+
*/
|
|
351
|
+
export const toolStepVisibleBelowCards = (toolName, cards) => toolName === SUBAGENT_TOOL_NAME || cards.every((card) => card.status === "approved" && !card.waitingForOthers);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocksdiy/react-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React common",
|
|
6
6
|
"keywords": [],
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"penpal": "^8.0.0",
|
|
40
40
|
"react": "^19.2.8",
|
|
41
41
|
"use-stick-to-bottom": "1.1.4",
|
|
42
|
-
"@blocksdiy/blocks-client-api": "1.
|
|
42
|
+
"@blocksdiy/blocks-client-api": "1.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^19.2.18",
|