@pikku/assistant-ui 0.12.4 → 0.12.6

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.
@@ -34,7 +34,7 @@ async function* parseSSEStream(reader) {
34
34
  * Shared helper: consume an SSE stream and populate text/toolCalls.
35
35
  * Returns an array of PendingApprovals when the stream requests them, or empty when done.
36
36
  */
37
- async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
37
+ async function processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinish) {
38
38
  const pendingApprovals = [];
39
39
  for await (const event of parseSSEStream(reader)) {
40
40
  switch (event.type) {
@@ -77,6 +77,31 @@ async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
77
77
  }
78
78
  break;
79
79
  }
80
+ case 'generative-ui': {
81
+ const nextPart = {
82
+ type: 'generative-ui',
83
+ spec: event.spec,
84
+ };
85
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'generative-ui');
86
+ if (existingIndex === -1)
87
+ structuredParts.push(nextPart);
88
+ else
89
+ structuredParts[existingIndex] = nextPart;
90
+ break;
91
+ }
92
+ case 'data': {
93
+ const nextPart = {
94
+ type: 'data',
95
+ name: event.name,
96
+ data: event.data,
97
+ };
98
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'data' && part.name === event.name);
99
+ if (existingIndex === -1)
100
+ structuredParts.push(nextPart);
101
+ else
102
+ structuredParts[existingIndex] = nextPart;
103
+ break;
104
+ }
80
105
  case 'approval-request':
81
106
  pendingApprovals.push({
82
107
  toolCallId: event.toolCallId,
@@ -151,13 +176,32 @@ export function resolvePikkuToolStatus(status, result) {
151
176
  return { type: 'error' };
152
177
  return { type: 'completed' };
153
178
  }
154
- function buildContent(text, toolCalls) {
179
+ function buildRichContent(text, structuredParts, toolCalls) {
155
180
  const content = [];
156
181
  if (text.value)
157
182
  content.push({ type: 'text', text: text.value });
183
+ content.push(...structuredParts);
158
184
  content.push(...toolCalls);
159
185
  return content;
160
186
  }
187
+ function buildContentFromAgentResult(result) {
188
+ const content = [];
189
+ if (typeof result === 'string') {
190
+ if (result)
191
+ content.push({ type: 'text', text: result });
192
+ return content;
193
+ }
194
+ if (!result || typeof result !== 'object')
195
+ return content;
196
+ const record = result;
197
+ if (typeof record.text === 'string' && record.text) {
198
+ content.push({ type: 'text', text: record.text });
199
+ }
200
+ if (record.ui != null) {
201
+ content.push({ type: 'generative-ui', spec: record.ui });
202
+ }
203
+ return content;
204
+ }
161
205
  function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
162
206
  return {
163
207
  async *run({ messages, abortSignal }) {
@@ -181,6 +225,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
181
225
  // The last resume triggers continuation (next LLM step).
182
226
  let lastText = { value: '' };
183
227
  let lastToolCalls = [];
228
+ let lastStructuredParts = [];
184
229
  let nextApprovals = [];
185
230
  for (let i = 0; i < decisions.length; i++) {
186
231
  const decision = decisions[i];
@@ -209,19 +254,21 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
209
254
  }
210
255
  const text = { value: '' };
211
256
  const toolCalls = [];
257
+ const structuredParts = [];
212
258
  const reader = resumeResponse.body.getReader();
213
- const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
259
+ const streamApprovals = await processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
214
260
  ? (onFinishRef.current ?? undefined)
215
261
  : undefined);
216
262
  // Keep the last resume's output (it has continuation content)
217
263
  lastText = text;
218
264
  lastToolCalls = toolCalls;
265
+ lastStructuredParts = structuredParts;
219
266
  if (streamApprovals.length > 0) {
220
267
  nextApprovals = streamApprovals;
221
268
  }
222
269
  }
223
270
  // Build content from the last resume's output
224
- const content = buildContent(lastText, lastToolCalls);
271
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
225
272
  if (nextApprovals.length > 0) {
226
273
  // More approvals from continuation — show them
227
274
  pendingApprovalsRef.current = nextApprovals;
@@ -249,7 +296,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
249
296
  lastToolCalls.push(approvalToolCall);
250
297
  }
251
298
  }
252
- const updatedContent = buildContent(lastText, lastToolCalls);
299
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
253
300
  yield {
254
301
  content: updatedContent,
255
302
  status: {
@@ -287,6 +334,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
287
334
  resourceId: opts.resourceId,
288
335
  model: opts.model,
289
336
  temperature: opts.temperature,
337
+ ...(opts.context ? { context: opts.context } : {}),
290
338
  }),
291
339
  signal: abortSignal,
292
340
  credentials: opts.credentials,
@@ -328,14 +376,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
328
376
  }
329
377
  const text = { value: '' };
330
378
  const toolCalls = [];
379
+ const structuredParts = [];
331
380
  let pendingContent = null;
332
381
  const yieldContent = () => {
333
- const content = buildContent(text, toolCalls);
382
+ const content = buildRichContent(text, structuredParts, toolCalls);
334
383
  if (content.length > 0)
335
384
  pendingContent = content;
336
385
  };
337
386
  const reader = response.body.getReader();
338
- const approvals = await processStream(reader, text, toolCalls, yieldContent, onFinishRef.current ?? undefined);
387
+ const approvals = await processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinishRef.current ?? undefined);
339
388
  if (approvals.length === 0) {
340
389
  // No approval needed — yield final content and done
341
390
  if (pendingContent) {
@@ -378,7 +427,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
378
427
  toolCalls.splice(i, 1);
379
428
  }
380
429
  }
381
- const content = buildContent(text, toolCalls);
430
+ const content = buildRichContent(text, structuredParts, toolCalls);
382
431
  yield {
383
432
  content,
384
433
  status: {
@@ -446,7 +495,12 @@ export const convertDbMessages = (dbMessages) => {
446
495
  continue;
447
496
  const parts = [];
448
497
  if (msg.content) {
449
- parts.push({ type: 'text', text: msg.content });
498
+ if (Array.isArray(msg.content)) {
499
+ parts.push(...msg.content);
500
+ }
501
+ else {
502
+ parts.push({ type: 'text', text: msg.content });
503
+ }
450
504
  }
451
505
  if (Array.isArray(msg.toolCalls)) {
452
506
  for (const tc of msg.toolCalls) {
@@ -533,6 +587,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
533
587
  // Resume uses SSE (same as streaming mode)
534
588
  let lastText = { value: '' };
535
589
  let lastToolCalls = [];
590
+ let lastStructuredParts = [];
536
591
  let nextApprovals = [];
537
592
  for (let i = 0; i < decisions.length; i++) {
538
593
  const decision = decisions[i];
@@ -560,17 +615,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
560
615
  }
561
616
  const text = { value: '' };
562
617
  const toolCalls = [];
618
+ const structuredParts = [];
563
619
  const reader = resumeResponse.body.getReader();
564
- const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
620
+ const streamApprovals = await processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
565
621
  ? (onFinishRef.current ?? undefined)
566
622
  : undefined);
567
623
  lastText = text;
568
624
  lastToolCalls = toolCalls;
625
+ lastStructuredParts = structuredParts;
569
626
  if (streamApprovals.length > 0) {
570
627
  nextApprovals = streamApprovals;
571
628
  }
572
629
  }
573
- const content = buildContent(lastText, lastToolCalls);
630
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
574
631
  if (nextApprovals.length > 0) {
575
632
  pendingApprovalsRef.current = nextApprovals;
576
633
  setPendingApprovalsRef.current(nextApprovals);
@@ -596,7 +653,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
596
653
  lastToolCalls.push(approvalToolCall);
597
654
  }
598
655
  }
599
- const updatedContent = buildContent(lastText, lastToolCalls);
656
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
600
657
  yield {
601
658
  content: updatedContent,
602
659
  status: {
@@ -632,6 +689,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
632
689
  resourceId: opts.resourceId,
633
690
  model: opts.model,
634
691
  temperature: opts.temperature,
692
+ ...(opts.context ? { context: opts.context } : {}),
635
693
  }),
636
694
  signal: abortSignal,
637
695
  credentials: opts.credentials,
@@ -656,9 +714,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
656
714
  },
657
715
  }));
658
716
  const content = [];
659
- if (json.result) {
660
- content.push({ type: 'text', text: json.result });
661
- }
717
+ content.push(...buildContentFromAgentResult(json.result));
662
718
  content.push(...toolCalls);
663
719
  yield {
664
720
  content,
@@ -671,10 +727,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
671
727
  }
672
728
  // No approvals — yield complete content
673
729
  onFinishRef.current?.();
674
- const content = [];
675
- if (json.result) {
676
- content.push({ type: 'text', text: String(json.result) });
677
- }
730
+ const content = buildContentFromAgentResult(json.result);
678
731
  if (content.length > 0) {
679
732
  yield { content };
680
733
  }
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
1
+ {"root":["../src/index.ts","../src/model-capabilities.ts","../src/use-file-attachment.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
1
+ {"root":["../src/index.ts","../src/model-capabilities.ts","../src/use-file-attachment.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "prepublishOnly": "yarn build"
21
21
  },
22
22
  "dependencies": {
23
- "@assistant-ui/react": "^0.12.12",
23
+ "@assistant-ui/react": "^0.12.12 || ^0.14.0",
24
24
  "react-markdown": "^10.1.0"
25
25
  },
26
26
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -17,3 +17,6 @@ export type {
17
17
  } from './use-pikku-agent-runtime.js'
18
18
  export { PikkuAgentChat } from './pikku-agent-chat.js'
19
19
  export type { PikkuAgentChatProps } from './pikku-agent-chat.js'
20
+ export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js'
21
+ export type { PendingFile, UploadAttachmentFn } from './use-file-attachment.js'
22
+ export { modelSupportsVision } from './model-capabilities.js'
@@ -0,0 +1,14 @@
1
+ const MODEL_VISION_SUPPORT = new Set([
2
+ 'claude-sonnet-4-5',
3
+ 'claude-opus-4-1',
4
+ 'gpt-4o-mini',
5
+ 'gpt-5',
6
+ 'gpt-5-mini',
7
+ 'gpt-5-codex',
8
+ 'gemini-2.5-pro',
9
+ 'gemini-2.5-flash',
10
+ ])
11
+
12
+ export function modelSupportsVision(modelID: string): boolean {
13
+ return MODEL_VISION_SUPPORT.has(modelID)
14
+ }