@pikku/assistant-ui 0.12.0 → 0.12.2

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.
@@ -1,6 +1,361 @@
1
- import { useMemo, useEffect, useRef, useCallback } from 'react';
2
- import { useDataStreamRuntime } from '@assistant-ui/react-data-stream';
3
- const convertDbMessages = (dbMessages) => {
1
+ import { createContext, useContext, useMemo, useRef, useState, useCallback, } from 'react';
2
+ import { useLocalRuntime, } from '@assistant-ui/react';
3
+ export const PikkuApprovalContext = createContext({
4
+ pendingApprovals: [],
5
+ handleApproval: () => { },
6
+ });
7
+ export const usePikkuApproval = () => useContext(PikkuApprovalContext);
8
+ async function* parseSSEStream(reader) {
9
+ const decoder = new TextDecoder();
10
+ let buffer = '';
11
+ while (true) {
12
+ const { done, value } = await reader.read();
13
+ if (done)
14
+ break;
15
+ buffer += decoder.decode(value, { stream: true });
16
+ const lines = buffer.split('\n');
17
+ buffer = lines.pop();
18
+ for (const line of lines) {
19
+ if (line.startsWith('data: ')) {
20
+ const data = line.slice(6);
21
+ if (data) {
22
+ try {
23
+ yield JSON.parse(data);
24
+ }
25
+ catch {
26
+ // skip unparseable lines
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Shared helper: consume an SSE stream and populate text/toolCalls.
35
+ * Returns an array of PendingApprovals when the stream requests them, or empty when done.
36
+ */
37
+ async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
38
+ const pendingApprovals = [];
39
+ for await (const event of parseSSEStream(reader)) {
40
+ switch (event.type) {
41
+ case 'text-delta':
42
+ text.value += event.text;
43
+ break;
44
+ case 'tool-call': {
45
+ let parsedArgs = event.args;
46
+ if (typeof event.args === 'string') {
47
+ try {
48
+ parsedArgs = JSON.parse(event.args);
49
+ }
50
+ catch {
51
+ parsedArgs = {};
52
+ }
53
+ }
54
+ toolCalls.push({
55
+ type: 'tool-call',
56
+ toolCallId: event.toolCallId,
57
+ toolName: event.toolName,
58
+ args: parsedArgs,
59
+ });
60
+ break;
61
+ }
62
+ case 'tool-result': {
63
+ // Skip tool-results that contain __approvalRequired
64
+ const resultObj = typeof event.result === 'object' ? event.result : null;
65
+ if (resultObj && '__approvalRequired' in resultObj) {
66
+ break;
67
+ }
68
+ const tc = toolCalls.find((t) => t.toolCallId === event.toolCallId);
69
+ if (tc) {
70
+ tc.result =
71
+ typeof event.result === 'string'
72
+ ? event.result
73
+ : JSON.stringify(event.result);
74
+ if (event.isError) {
75
+ tc.isError = true;
76
+ }
77
+ }
78
+ break;
79
+ }
80
+ case 'approval-request':
81
+ pendingApprovals.push({
82
+ toolCallId: event.toolCallId,
83
+ toolName: event.toolName,
84
+ args: event.args,
85
+ reason: event.reason,
86
+ runId: event.runId,
87
+ });
88
+ break;
89
+ case 'error':
90
+ text.value += `\n\nError: ${event.message || event.errorText || 'Unknown error'}`;
91
+ break;
92
+ case 'done':
93
+ onFinish?.();
94
+ continue;
95
+ }
96
+ yieldContent();
97
+ }
98
+ return pendingApprovals;
99
+ }
100
+ export function isDeniedResult(result) {
101
+ if (result == null)
102
+ return false;
103
+ try {
104
+ const parsed = typeof result === 'string' ? JSON.parse(result) : result;
105
+ return parsed && typeof parsed === 'object' && parsed.approved === false;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ export function resolvePikkuToolStatus(status, result) {
112
+ if (status.type === 'running')
113
+ return { type: 'running' };
114
+ if (status.type === 'requires-action')
115
+ return { type: 'requires-action' };
116
+ if (isDeniedResult(result))
117
+ return { type: 'denied' };
118
+ if (typeof result === 'string' && result.startsWith('Error:'))
119
+ return { type: 'error' };
120
+ return { type: 'completed' };
121
+ }
122
+ function buildContent(text, toolCalls) {
123
+ const content = [];
124
+ if (text.value)
125
+ content.push({ type: 'text', text: text.value });
126
+ content.push(...toolCalls);
127
+ return content;
128
+ }
129
+ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
130
+ return {
131
+ async *run({ messages, abortSignal }) {
132
+ const opts = optionsRef.current;
133
+ // Check if this run() is a continuation after approval decisions.
134
+ // assistant-ui calls run() again after addResult provides tool results for ALL tool calls.
135
+ const pendingApprovals = pendingApprovalsRef.current;
136
+ if (pendingApprovals.length > 0) {
137
+ // Read the decisions accumulated by handleApproval() from click handlers.
138
+ const decisions = approvalDecisionsRef.current;
139
+ approvalDecisionsRef.current = [];
140
+ if (decisions.length === 0) {
141
+ // No decisions set — shouldn't happen if composer is disabled.
142
+ return;
143
+ }
144
+ // Clear pending approvals state
145
+ pendingApprovalsRef.current = [];
146
+ setPendingApprovalsRef.current([]);
147
+ // Send /resume for each decision sequentially.
148
+ // All but the last resume will return quickly (just tool-result + done).
149
+ // The last resume triggers continuation (next LLM step).
150
+ let lastText = { value: '' };
151
+ let lastToolCalls = [];
152
+ let nextApprovals = [];
153
+ for (let i = 0; i < decisions.length; i++) {
154
+ const decision = decisions[i];
155
+ // Find the runId from the matching pending approval
156
+ const matchingApproval = pendingApprovals.find((p) => p.toolCallId === decision.toolCallId);
157
+ const runId = matchingApproval?.runId ?? pendingApprovals[0]?.runId;
158
+ const resumeResponse = await fetch(`${opts.api}/${opts.agentName}/resume`, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ ...opts.headers,
163
+ },
164
+ body: JSON.stringify({
165
+ runId,
166
+ toolCallId: decision.toolCallId,
167
+ approved: decision.approved,
168
+ }),
169
+ signal: abortSignal,
170
+ credentials: opts.credentials,
171
+ });
172
+ if (!resumeResponse.ok || !resumeResponse.body) {
173
+ const errorText = resumeResponse.body
174
+ ? await resumeResponse.text().catch(() => '')
175
+ : '';
176
+ throw new Error(`Resume failed: ${resumeResponse.status}${errorText ? ` - ${errorText}` : ''}`);
177
+ }
178
+ const text = { value: '' };
179
+ const toolCalls = [];
180
+ const reader = resumeResponse.body.getReader();
181
+ const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
182
+ ? (onFinishRef.current ?? undefined)
183
+ : undefined);
184
+ // Keep the last resume's output (it has continuation content)
185
+ lastText = text;
186
+ lastToolCalls = toolCalls;
187
+ if (streamApprovals.length > 0) {
188
+ nextApprovals = streamApprovals;
189
+ }
190
+ }
191
+ // Build content from the last resume's output
192
+ const content = buildContent(lastText, lastToolCalls);
193
+ if (nextApprovals.length > 0) {
194
+ // More approvals from continuation — show them
195
+ pendingApprovalsRef.current = nextApprovals;
196
+ setPendingApprovalsRef.current(nextApprovals);
197
+ // Add approval tool calls to content
198
+ for (const approval of nextApprovals) {
199
+ const approvalToolCall = {
200
+ type: 'tool-call',
201
+ toolCallId: approval.toolCallId,
202
+ toolName: approval.toolName,
203
+ args: {
204
+ ...(typeof approval.args === 'object' && approval.args !== null
205
+ ? approval.args
206
+ : {}),
207
+ ...(approval.reason
208
+ ? { __approvalReason: approval.reason }
209
+ : {}),
210
+ },
211
+ };
212
+ const idx = lastToolCalls.findIndex((tc) => tc.toolCallId === approval.toolCallId && !tc.result);
213
+ if (idx !== -1) {
214
+ lastToolCalls[idx] = approvalToolCall;
215
+ }
216
+ else {
217
+ lastToolCalls.push(approvalToolCall);
218
+ }
219
+ }
220
+ const updatedContent = buildContent(lastText, lastToolCalls);
221
+ yield {
222
+ content: updatedContent,
223
+ status: {
224
+ type: 'requires-action',
225
+ reason: 'tool-calls',
226
+ },
227
+ };
228
+ }
229
+ else if (content.length > 0) {
230
+ yield { content };
231
+ }
232
+ return;
233
+ }
234
+ // Normal flow: new user message → stream
235
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
236
+ if (!lastUserMsg)
237
+ return;
238
+ let messageText = '';
239
+ if (lastUserMsg.content) {
240
+ for (const part of lastUserMsg.content) {
241
+ if ('text' in part && part.type === 'text') {
242
+ messageText += part.text;
243
+ }
244
+ }
245
+ }
246
+ let response;
247
+ try {
248
+ response = await fetch(`${opts.api}/${opts.agentName}/stream`, {
249
+ method: 'POST',
250
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
251
+ body: JSON.stringify({
252
+ agentName: opts.agentName,
253
+ message: messageText,
254
+ threadId: opts.threadId,
255
+ resourceId: opts.resourceId,
256
+ model: opts.model,
257
+ temperature: opts.temperature,
258
+ }),
259
+ signal: abortSignal,
260
+ credentials: opts.credentials,
261
+ });
262
+ }
263
+ catch (e) {
264
+ const msg = e?.message || 'Unknown error';
265
+ let errorText = 'Failed to connect to the agent.';
266
+ if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || msg.includes('CORS')) {
267
+ errorText = 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.';
268
+ }
269
+ else if (msg.includes('abort')) {
270
+ return;
271
+ }
272
+ yield { content: [{ type: 'text', text: `⚠️ ${errorText}` }] };
273
+ return;
274
+ }
275
+ if (!response.ok || !response.body) {
276
+ let errorText = `Request failed (${response.status}).`;
277
+ if (response.status === 401 || response.status === 403) {
278
+ errorText = 'Authentication error.';
279
+ }
280
+ else if (response.status === 404) {
281
+ errorText = 'Agent not found — the agent may not be configured for this project.';
282
+ }
283
+ else if (response.status === 502 || response.status === 503) {
284
+ errorText = 'The agent server is currently unavailable. Try again in a moment.';
285
+ }
286
+ else if (response.status === 429) {
287
+ errorText = 'Rate limited — too many requests. Please wait a moment.';
288
+ }
289
+ yield { content: [{ type: 'text', text: `⚠️ ${errorText}` }] };
290
+ return;
291
+ }
292
+ const text = { value: '' };
293
+ const toolCalls = [];
294
+ let pendingContent = null;
295
+ const yieldContent = () => {
296
+ const content = buildContent(text, toolCalls);
297
+ if (content.length > 0)
298
+ pendingContent = content;
299
+ };
300
+ const reader = response.body.getReader();
301
+ const approvals = await processStream(reader, text, toolCalls, yieldContent, onFinishRef.current ?? undefined);
302
+ if (approvals.length === 0) {
303
+ // No approval needed — yield final content and done
304
+ if (pendingContent) {
305
+ yield { content: pendingContent };
306
+ }
307
+ return;
308
+ }
309
+ // Approvals requested: store them for the next run() call
310
+ pendingApprovalsRef.current = approvals;
311
+ setPendingApprovalsRef.current(approvals);
312
+ // Each approval tool call needs to be shown without a result
313
+ // so assistant-ui renders them as requires-action
314
+ for (const approval of approvals) {
315
+ const approvalToolCall = {
316
+ type: 'tool-call',
317
+ toolCallId: approval.toolCallId,
318
+ toolName: approval.toolName,
319
+ args: {
320
+ ...(typeof approval.args === 'object' && approval.args !== null
321
+ ? approval.args
322
+ : {}),
323
+ ...(approval.reason ? { __approvalReason: approval.reason } : {}),
324
+ },
325
+ };
326
+ // Replace the existing tool call (if any) with the approval version
327
+ const parentIdx = toolCalls.findIndex((tc) => tc.toolCallId === approval.toolCallId && !tc.result);
328
+ if (parentIdx !== -1) {
329
+ toolCalls[parentIdx] = approvalToolCall;
330
+ }
331
+ else {
332
+ toolCalls.push(approvalToolCall);
333
+ }
334
+ }
335
+ // Remove any forwarded sub-agent tool calls that duplicate approval tool names
336
+ const approvalToolCallIds = new Set(approvals.map((a) => a.toolCallId));
337
+ const approvalToolNames = new Set(approvals.map((a) => a.toolName));
338
+ for (let i = toolCalls.length - 1; i >= 0; i--) {
339
+ if (approvalToolNames.has(toolCalls[i].toolName) &&
340
+ !approvalToolCallIds.has(toolCalls[i].toolCallId)) {
341
+ toolCalls.splice(i, 1);
342
+ }
343
+ }
344
+ const content = buildContent(text, toolCalls);
345
+ yield {
346
+ content,
347
+ status: {
348
+ type: 'requires-action',
349
+ reason: 'tool-calls',
350
+ },
351
+ };
352
+ // Generator returns here. assistant-ui will show the approval UI for each tool call.
353
+ // When the user clicks Approve/Deny on ALL tools → handleApproval accumulates decisions →
354
+ // addResult for each → run() is called again when all have results.
355
+ },
356
+ };
357
+ }
358
+ export const convertDbMessages = (dbMessages) => {
4
359
  const result = [];
5
360
  let currentAssistant = null;
6
361
  for (const msg of dbMessages) {
@@ -97,67 +452,221 @@ const convertDbMessages = (dbMessages) => {
97
452
  return result;
98
453
  };
99
454
  export function usePikkuAgentRuntime(options) {
100
- const { api, threadId = null, initialMessages: rawInitialMessages, onThreadCreated, onFinish, onApprovalRequest, credentials, headers, } = options;
101
- const threadIdRef = useRef(threadId);
102
- threadIdRef.current = threadId;
103
- const justCreatedThreadRef = useRef(false);
104
- const bodyFn = useCallback(() => {
105
- let currentThreadId = threadIdRef.current;
106
- if (!currentThreadId) {
107
- currentThreadId = crypto.randomUUID();
108
- justCreatedThreadRef.current = true;
109
- onThreadCreated?.(currentThreadId);
110
- }
111
- return { threadId: currentThreadId };
112
- }, [onThreadCreated]);
113
- const onData = useCallback((event) => {
114
- if (event.name === 'approval-request') {
115
- const approval = event.data;
116
- onApprovalRequest?.({
117
- toolCallId: approval.toolCallId,
455
+ const [pendingApprovals, setPendingApprovals] = useState([]);
456
+ const optionsRef = useRef(options);
457
+ optionsRef.current = options;
458
+ const onFinishRef = useRef(options.onFinish);
459
+ onFinishRef.current = options.onFinish;
460
+ const pendingApprovalsRef = useRef([]);
461
+ const approvalDecisionsRef = useRef([]);
462
+ const setPendingApprovalsRef = useRef(setPendingApprovals);
463
+ setPendingApprovalsRef.current = setPendingApprovals;
464
+ const adapter = useMemo(() => createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef), []);
465
+ const initialMessages = useMemo(() => options.initialMessages
466
+ ? convertDbMessages(options.initialMessages)
467
+ : undefined, [options.initialMessages]);
468
+ const runtime = useLocalRuntime(adapter, { initialMessages });
469
+ // handleApproval is called from the Approve/Deny button click handler.
470
+ // It accumulates the decision in the ref. assistant-ui will call run()
471
+ // when ALL tool calls have results (via addResult).
472
+ const handleApproval = useCallback((toolCallId, approved) => {
473
+ approvalDecisionsRef.current.push({ toolCallId, approved });
474
+ }, []);
475
+ const isAwaitingApproval = pendingApprovals.length > 0;
476
+ return {
477
+ runtime,
478
+ pendingApprovals,
479
+ isAwaitingApproval,
480
+ handleApproval,
481
+ };
482
+ }
483
+ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
484
+ return {
485
+ async *run({ messages, abortSignal }) {
486
+ const opts = optionsRef.current;
487
+ // Continuation after approval decisions
488
+ const pendingApprovals = pendingApprovalsRef.current;
489
+ if (pendingApprovals.length > 0) {
490
+ const decisions = approvalDecisionsRef.current;
491
+ approvalDecisionsRef.current = [];
492
+ if (decisions.length === 0)
493
+ return;
494
+ pendingApprovalsRef.current = [];
495
+ setPendingApprovalsRef.current([]);
496
+ // Resume uses SSE (same as streaming mode)
497
+ let lastText = { value: '' };
498
+ let lastToolCalls = [];
499
+ let nextApprovals = [];
500
+ for (let i = 0; i < decisions.length; i++) {
501
+ const decision = decisions[i];
502
+ const matchingApproval = pendingApprovals.find((p) => p.toolCallId === decision.toolCallId);
503
+ const runId = matchingApproval?.runId ?? pendingApprovals[0]?.runId;
504
+ const resumeResponse = await fetch(`${opts.api}/${opts.agentName}/resume`, {
505
+ method: 'POST',
506
+ headers: {
507
+ 'Content-Type': 'application/json',
508
+ ...opts.headers,
509
+ },
510
+ body: JSON.stringify({
511
+ runId,
512
+ toolCallId: decision.toolCallId,
513
+ approved: decision.approved,
514
+ }),
515
+ signal: abortSignal,
516
+ credentials: opts.credentials,
517
+ });
518
+ if (!resumeResponse.ok || !resumeResponse.body) {
519
+ const errorText = resumeResponse.body
520
+ ? await resumeResponse.text().catch(() => '')
521
+ : '';
522
+ throw new Error(`Resume failed: ${resumeResponse.status}${errorText ? ` - ${errorText}` : ''}`);
523
+ }
524
+ const text = { value: '' };
525
+ const toolCalls = [];
526
+ const reader = resumeResponse.body.getReader();
527
+ const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
528
+ ? (onFinishRef.current ?? undefined)
529
+ : undefined);
530
+ lastText = text;
531
+ lastToolCalls = toolCalls;
532
+ if (streamApprovals.length > 0) {
533
+ nextApprovals = streamApprovals;
534
+ }
535
+ }
536
+ const content = buildContent(lastText, lastToolCalls);
537
+ if (nextApprovals.length > 0) {
538
+ pendingApprovalsRef.current = nextApprovals;
539
+ setPendingApprovalsRef.current(nextApprovals);
540
+ for (const approval of nextApprovals) {
541
+ const approvalToolCall = {
542
+ type: 'tool-call',
543
+ toolCallId: approval.toolCallId,
544
+ toolName: approval.toolName,
545
+ args: {
546
+ ...(typeof approval.args === 'object' && approval.args !== null
547
+ ? approval.args
548
+ : {}),
549
+ ...(approval.reason
550
+ ? { __approvalReason: approval.reason }
551
+ : {}),
552
+ },
553
+ };
554
+ const idx = lastToolCalls.findIndex((tc) => tc.toolCallId === approval.toolCallId && !tc.result);
555
+ if (idx !== -1) {
556
+ lastToolCalls[idx] = approvalToolCall;
557
+ }
558
+ else {
559
+ lastToolCalls.push(approvalToolCall);
560
+ }
561
+ }
562
+ const updatedContent = buildContent(lastText, lastToolCalls);
563
+ yield {
564
+ content: updatedContent,
565
+ status: {
566
+ type: 'requires-action',
567
+ reason: 'tool-calls',
568
+ },
569
+ };
570
+ }
571
+ else if (content.length > 0) {
572
+ yield { content };
573
+ }
574
+ return;
575
+ }
576
+ // Normal flow: new user message → non-streaming POST
577
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
578
+ if (!lastUserMsg)
579
+ return;
580
+ let messageText = '';
581
+ if (lastUserMsg.content) {
582
+ for (const part of lastUserMsg.content) {
583
+ if ('text' in part && part.type === 'text') {
584
+ messageText += part.text;
585
+ }
586
+ }
587
+ }
588
+ const response = await fetch(`${opts.api}/${opts.agentName}`, {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
591
+ body: JSON.stringify({
592
+ agentName: opts.agentName,
593
+ message: messageText,
594
+ threadId: opts.threadId,
595
+ resourceId: opts.resourceId,
596
+ model: opts.model,
597
+ temperature: opts.temperature,
598
+ }),
599
+ signal: abortSignal,
600
+ credentials: opts.credentials,
118
601
  });
119
- }
120
- }, [onApprovalRequest]);
121
- const onFinishCb = useCallback(() => {
122
- onFinish?.();
123
- }, [onFinish]);
124
- const initialMessages = useMemo(() => (rawInitialMessages ? convertDbMessages(rawInitialMessages) : []), [rawInitialMessages]);
125
- const runtime = useDataStreamRuntime({
126
- api,
127
- protocol: 'ui-message-stream',
128
- body: bodyFn,
129
- onData,
130
- onFinish: onFinishCb,
131
- initialMessages,
132
- credentials,
133
- headers,
134
- });
135
- const prevThreadIdRef = useRef(threadId);
136
- const hasResetRef = useRef(false);
137
- useEffect(() => {
138
- if (prevThreadIdRef.current !== threadId) {
139
- prevThreadIdRef.current = threadId;
140
- if (justCreatedThreadRef.current) {
141
- justCreatedThreadRef.current = false;
142
- hasResetRef.current = true;
602
+ if (!response.ok) {
603
+ throw new Error(`Agent run failed: ${response.status}`);
604
+ }
605
+ const json = await response.json();
606
+ if (json.status === 'suspended' && json.pendingApprovals?.length > 0) {
607
+ const approvals = json.pendingApprovals;
608
+ pendingApprovalsRef.current = approvals;
609
+ setPendingApprovalsRef.current(approvals);
610
+ const toolCalls = approvals.map((approval) => ({
611
+ type: 'tool-call',
612
+ toolCallId: approval.toolCallId,
613
+ toolName: approval.toolName,
614
+ args: {
615
+ ...(typeof approval.args === 'object' && approval.args !== null
616
+ ? approval.args
617
+ : {}),
618
+ ...(approval.reason ? { __approvalReason: approval.reason } : {}),
619
+ },
620
+ }));
621
+ const content = [];
622
+ if (json.result) {
623
+ content.push({ type: 'text', text: json.result });
624
+ }
625
+ content.push(...toolCalls);
626
+ yield {
627
+ content,
628
+ status: {
629
+ type: 'requires-action',
630
+ reason: 'tool-calls',
631
+ },
632
+ };
143
633
  return;
144
634
  }
145
- hasResetRef.current = false;
146
- if (rawInitialMessages) {
147
- ;
148
- runtime.thread.reset(convertDbMessages(rawInitialMessages));
149
- hasResetRef.current = true;
635
+ // No approvals — yield complete content
636
+ onFinishRef.current?.();
637
+ const content = [];
638
+ if (json.result) {
639
+ content.push({ type: 'text', text: String(json.result) });
150
640
  }
151
- else {
152
- ;
153
- runtime.thread.reset([]);
641
+ if (content.length > 0) {
642
+ yield { content };
154
643
  }
155
- }
156
- else if (!hasResetRef.current && rawInitialMessages) {
157
- ;
158
- runtime.thread.reset(convertDbMessages(rawInitialMessages));
159
- hasResetRef.current = true;
160
- }
161
- }, [threadId, rawInitialMessages, runtime]);
162
- return runtime;
644
+ },
645
+ };
646
+ }
647
+ export function usePikkuAgentNonStreamingRuntime(options) {
648
+ const [pendingApprovals, setPendingApprovals] = useState([]);
649
+ const optionsRef = useRef(options);
650
+ optionsRef.current = options;
651
+ const onFinishRef = useRef(options.onFinish);
652
+ onFinishRef.current = options.onFinish;
653
+ const pendingApprovalsRef = useRef([]);
654
+ const approvalDecisionsRef = useRef([]);
655
+ const setPendingApprovalsRef = useRef(setPendingApprovals);
656
+ setPendingApprovalsRef.current = setPendingApprovals;
657
+ const adapter = useMemo(() => createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef), []);
658
+ const initialMessages = useMemo(() => options.initialMessages
659
+ ? convertDbMessages(options.initialMessages)
660
+ : undefined, [options.initialMessages]);
661
+ const runtime = useLocalRuntime(adapter, { initialMessages });
662
+ const handleApproval = useCallback((toolCallId, approved) => {
663
+ approvalDecisionsRef.current.push({ toolCallId, approved });
664
+ }, []);
665
+ const isAwaitingApproval = pendingApprovals.length > 0;
666
+ return {
667
+ runtime,
668
+ pendingApprovals,
669
+ isAwaitingApproval,
670
+ handleApproval,
671
+ };
163
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,11 +16,12 @@
16
16
  "tsc": "tsc",
17
17
  "build:esm": "tsc -b && echo '{\"type\": \"module\"}' > dist/esm/package.json",
18
18
  "build:cjs": "tsc -b tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
19
- "build": "yarn build:esm && yarn build:cjs"
19
+ "build": "yarn build:esm && yarn build:cjs",
20
+ "prepublishOnly": "yarn build"
20
21
  },
21
22
  "dependencies": {
22
23
  "@assistant-ui/react": "^0.12.12",
23
- "@assistant-ui/react-data-stream": "^0.12.5"
24
+ "react-markdown": "^10.1.0"
24
25
  },
25
26
  "peerDependencies": {
26
27
  "react": "^18 || ^19",