@pikku/assistant-ui 0.12.3 → 0.12.5

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,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useMemo } from 'react';
2
+ import { createContext, useContext, useState, useMemo, useEffect, useRef, } from 'react';
3
3
  import Markdown from 'react-markdown';
4
- import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, } from '@assistant-ui/react';
4
+ import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, useComposerRuntime, } from '@assistant-ui/react';
5
5
  import { usePikkuAgentRuntime, PikkuApprovalContext, usePikkuApproval, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
6
6
  const lightColors = {
7
7
  bg: '#ffffff',
@@ -45,6 +45,9 @@ const darkColors = {
45
45
  };
46
46
  const ColorsContext = createContext(lightColors);
47
47
  const HideToolCallsContext = createContext(undefined);
48
+ const ToolComponentsContext = createContext(undefined);
49
+ const GenerativeUIComponentsContext = createContext(undefined);
50
+ const RenderAssistantTextContext = createContext(undefined);
48
51
  function shouldHideToolCall(hideToolCalls, toolName) {
49
52
  if (!hideToolCalls)
50
53
  return false;
@@ -135,7 +138,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
135
138
  padding: '2px 6px',
136
139
  borderRadius: 3,
137
140
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
138
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
141
+ color: responded === 'approved'
142
+ ? colors.successColor
143
+ : colors.errorColor,
139
144
  }, children: responded })] }));
140
145
  }
141
146
  return (_jsxs("div", { style: {
@@ -185,7 +190,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
185
190
  borderRadius: 4,
186
191
  overflow: 'auto',
187
192
  color: colors.text,
188
- }, children: JSON.stringify(displayArgs, null, 2) }), result !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: 12, color: colors.textMuted, marginTop: 8, marginBottom: 2 }, children: "Result:" }), _jsx("pre", { style: {
193
+ }, children: JSON.stringify(displayArgs, null, 2) }), result !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
194
+ fontSize: 12,
195
+ color: colors.textMuted,
196
+ marginTop: 8,
197
+ marginBottom: 2,
198
+ }, children: "Result:" }), _jsx("pre", { style: {
189
199
  fontSize: 11,
190
200
  background: colors.codeBg,
191
201
  padding: 8,
@@ -196,25 +206,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
196
206
  ? result
197
207
  : JSON.stringify(result, null, 2) })] }))] }))] }));
198
208
  };
199
- const MarkdownText = ({ text, colors }) => {
209
+ const MarkdownText = ({ text, colors, }) => {
200
210
  const components = useMemo(() => ({
201
- p: ({ children }) => (_jsx("p", { style: { margin: '0 0 8px', fontSize: 14, lineHeight: 1.6, color: colors.text }, children: children })),
211
+ p: ({ children }) => (_jsx("p", { style: {
212
+ margin: '0 0 8px',
213
+ fontSize: 14,
214
+ lineHeight: 1.6,
215
+ color: colors.text,
216
+ }, children: children })),
202
217
  strong: ({ children }) => (_jsx("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
203
218
  em: ({ children }) => (_jsx("em", { style: { color: colors.text }, children: children })),
204
- ul: ({ children }) => (_jsx("ul", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
205
- ol: ({ children }) => (_jsx("ol", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
219
+ ul: ({ children }) => (_jsx("ul", { style: {
220
+ margin: '4px 0 8px',
221
+ paddingLeft: 20,
222
+ fontSize: 14,
223
+ color: colors.text,
224
+ }, children: children })),
225
+ ol: ({ children }) => (_jsx("ol", { style: {
226
+ margin: '4px 0 8px',
227
+ paddingLeft: 20,
228
+ fontSize: 14,
229
+ color: colors.text,
230
+ }, children: children })),
206
231
  li: ({ children }) => (_jsx("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
207
232
  code: ({ children, className }) => {
208
233
  const isBlock = className?.startsWith('language-');
209
234
  if (isBlock) {
210
- return (_jsx("pre", { style: { background: colors.codeBg, padding: 10, borderRadius: 4, overflow: 'auto', margin: '4px 0 8px', fontSize: 12 }, children: _jsx("code", { style: { color: colors.text }, children: children }) }));
235
+ return (_jsx("pre", { style: {
236
+ background: colors.codeBg,
237
+ padding: 10,
238
+ borderRadius: 4,
239
+ overflow: 'auto',
240
+ margin: '4px 0 8px',
241
+ fontSize: 12,
242
+ }, children: _jsx("code", { style: { color: colors.text }, children: children }) }));
211
243
  }
212
- return (_jsx("code", { style: { background: colors.codeBg, padding: '1px 4px', borderRadius: 3, fontSize: 13, color: colors.text }, children: children }));
244
+ return (_jsx("code", { style: {
245
+ background: colors.codeBg,
246
+ padding: '1px 4px',
247
+ borderRadius: 3,
248
+ fontSize: 13,
249
+ color: colors.text,
250
+ }, children: children }));
213
251
  },
214
252
  pre: ({ children }) => _jsx(_Fragment, { children: children }),
215
- h1: ({ children }) => _jsx("h3", { style: { margin: '8px 0 4px', fontSize: 16, fontWeight: 600, color: colors.text }, children: children }),
216
- h2: ({ children }) => _jsx("h4", { style: { margin: '8px 0 4px', fontSize: 15, fontWeight: 600, color: colors.text }, children: children }),
217
- h3: ({ children }) => _jsx("h5", { style: { margin: '8px 0 4px', fontSize: 14, fontWeight: 600, color: colors.text }, children: children }),
253
+ h1: ({ children }) => (_jsx("h3", { style: {
254
+ margin: '8px 0 4px',
255
+ fontSize: 16,
256
+ fontWeight: 600,
257
+ color: colors.text,
258
+ }, children: children })),
259
+ h2: ({ children }) => (_jsx("h4", { style: {
260
+ margin: '8px 0 4px',
261
+ fontSize: 15,
262
+ fontWeight: 600,
263
+ color: colors.text,
264
+ }, children: children })),
265
+ h3: ({ children }) => (_jsx("h5", { style: {
266
+ margin: '8px 0 4px',
267
+ fontSize: 14,
268
+ fontWeight: 600,
269
+ color: colors.text,
270
+ }, children: children })),
218
271
  a: ({ href, children }) => (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", style: { color: colors.textMuted, textDecoration: 'underline' }, children: children })),
219
272
  }), [colors]);
220
273
  return _jsx(Markdown, { components: components, children: text });
@@ -235,11 +288,18 @@ const UserMessage = () => {
235
288
  borderRadius: 12,
236
289
  backgroundColor: colors.userBubble,
237
290
  }, children: _jsx(MessagePrimitive.Content, { components: {
238
- Text: ({ text }) => (_jsx("span", { style: { fontSize: 14, whiteSpace: 'pre-wrap', color: colors.text }, children: text })),
291
+ Text: ({ text }) => (_jsx("span", { style: {
292
+ fontSize: 14,
293
+ whiteSpace: 'pre-wrap',
294
+ color: colors.text,
295
+ }, children: text })),
239
296
  } }) })] }) }));
240
297
  };
241
298
  const AssistantMessage = () => {
242
299
  const colors = useContext(ColorsContext);
300
+ const toolComponents = useContext(ToolComponentsContext);
301
+ const generativeUIComponents = useContext(GenerativeUIComponentsContext);
302
+ const renderAssistantText = useContext(RenderAssistantTextContext);
243
303
  return (_jsx("div", { style: {
244
304
  display: 'flex',
245
305
  justifyContent: 'flex-start',
@@ -249,10 +309,18 @@ const AssistantMessage = () => {
249
309
  borderRadius: 12,
250
310
  backgroundColor: colors.assistantBubble,
251
311
  }, children: [_jsx(MessagePrimitive.Content, { components: {
252
- Text: ({ text }) => (_jsx(MarkdownText, { text: text, colors: colors })),
312
+ Text: ({ text }) => renderAssistantText ? (_jsx(_Fragment, { children: renderAssistantText(text) })) : (_jsx(MarkdownText, { text: text, colors: colors })),
253
313
  tools: {
314
+ by_name: toolComponents,
254
315
  Fallback: (props) => (_jsx(ToolCallDisplay, { toolCallId: props.toolCallId, toolName: props.toolName, args: props.args, result: props.result, status: resolvePikkuToolStatus(props.status, props.result), addResult: props.addResult })),
255
316
  },
317
+ ...(generativeUIComponents
318
+ ? {
319
+ generativeUI: {
320
+ components: generativeUIComponents,
321
+ },
322
+ }
323
+ : {}),
256
324
  } }), _jsx(MessagePrimitive.If, { last: true, children: _jsx(ThreadPrimitive.If, { running: true, children: _jsx("div", { style: {
257
325
  display: 'flex',
258
326
  alignItems: 'center',
@@ -262,81 +330,112 @@ const AssistantMessage = () => {
262
330
  color: colors.textMuted,
263
331
  }, children: "Thinking..." }) }) })] })] }) }));
264
332
  };
333
+ const ComposerPrefill = ({ text }) => {
334
+ const composer = useComposerRuntime();
335
+ const filled = useRef(false);
336
+ useEffect(() => {
337
+ if (filled.current || !text)
338
+ return;
339
+ filled.current = true;
340
+ if (composer.getState().text === '')
341
+ composer.setText(text);
342
+ }, [text, composer]);
343
+ return null;
344
+ };
265
345
  const PikkuComposer = ({ disabled, }) => {
266
346
  const colors = useContext(ColorsContext);
267
347
  return (_jsx("div", { style: { padding: '8px 0 16px' }, children: _jsx(ComposerPrimitive.Root, { children: _jsxs("div", { style: {
268
- border: `1px solid ${colors.border}`,
269
- borderRadius: 12,
270
- overflow: 'hidden',
348
+ position: 'relative',
349
+ zIndex: 2,
271
350
  display: 'flex',
272
- alignItems: 'flex-end',
273
- padding: '6px 12px',
274
- gap: 8,
275
- ...(disabled ? { opacity: 0.5, pointerEvents: 'none' } : {}),
276
- }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
277
- flex: 1,
351
+ flexDirection: 'column',
352
+ gap: 10,
353
+ backgroundColor: colors.assistantBubble,
354
+ border: `1px solid ${colors.border}`,
355
+ borderRadius: 24,
356
+ padding: '14px 12px 10px',
357
+ boxShadow: darkColors.text === colors.text
358
+ ? '0 14px 30px rgba(0,0,0,0.24)'
359
+ : '0 14px 30px rgba(0,0,0,0.08)',
360
+ ...(disabled
361
+ ? { opacity: 0.5, pointerEvents: 'none' }
362
+ : {}),
363
+ }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 1, disabled: disabled ?? false, style: {
364
+ width: '100%',
365
+ background: 'transparent',
278
366
  border: 'none',
279
367
  outline: 'none',
280
- resize: 'none',
368
+ color: colors.text,
281
369
  fontSize: 14,
282
370
  fontFamily: 'inherit',
283
- padding: '4px 0',
284
- background: colors.inputBg,
285
- color: colors.text,
286
- } }), _jsx(ComposerPrimitive.Send, { disabled: disabled, style: {
287
- width: 28,
288
- height: 28,
289
- borderRadius: '50%',
290
- border: 'none',
291
- background: disabled ? colors.textMuted : colors.sendBg,
292
- color: colors.sendColor,
293
- cursor: disabled ? 'not-allowed' : 'pointer',
371
+ resize: 'none',
372
+ lineHeight: 1.5,
373
+ overflowY: 'auto',
374
+ } }), _jsx("div", { style: {
375
+ width: '100%',
294
376
  display: 'flex',
295
377
  alignItems: 'center',
296
- justifyContent: 'center',
297
- flexShrink: 0,
298
- marginBottom: 2,
299
- fontSize: 14,
300
- }, children: "\u25B6" })] }) }) }));
378
+ gap: 6,
379
+ minWidth: 0,
380
+ justifyContent: 'flex-end',
381
+ }, children: _jsx(ComposerPrimitive.Send, { disabled: disabled ?? false, style: {
382
+ width: 30,
383
+ height: 30,
384
+ borderRadius: 999,
385
+ border: `1px solid ${colors.border}`,
386
+ background: '#e8e8e8',
387
+ color: '#111111',
388
+ cursor: disabled ? 'not-allowed' : 'pointer',
389
+ display: 'flex',
390
+ alignItems: 'center',
391
+ justifyContent: 'center',
392
+ flexShrink: 0,
393
+ transition: 'background-color 150ms ease, color 150ms ease, border-color 150ms ease',
394
+ }, children: "\u2191" }) })] }) }) }));
301
395
  };
302
396
  export function PikkuAgentChat(props) {
303
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768, ...runtimeOptions } = props;
397
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, renderAssistantText, generativeUIComponents, initialPrompt, ...runtimeOptions } = props;
304
398
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
305
399
  const colors = dark ? darkColors : lightColors;
306
- return (_jsx(ColorsContext.Provider, { value: colors, children: _jsx(PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: _jsx(HideToolCallsContext.Provider, { value: hideToolCalls, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsx("div", { style: {
307
- height: '100%',
308
- display: 'flex',
309
- flexDirection: 'column',
310
- background: colors.bg,
311
- }, children: _jsxs(ThreadPrimitive.Root, { style: {
312
- display: 'flex',
313
- flexDirection: 'column',
314
- flex: 1,
315
- minHeight: 0,
316
- }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
317
- flex: 1,
318
- minHeight: 0,
319
- overflowY: 'auto',
320
- }, children: _jsxs("div", { style: {
321
- maxWidth: maxWidth === 'none' ? undefined : maxWidth,
322
- margin: '0 auto',
323
- padding: 16,
400
+ return (_jsx(ColorsContext.Provider, { value: colors, children: _jsx(PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: _jsx(HideToolCallsContext.Provider, { value: hideToolCalls, children: _jsx(ToolComponentsContext.Provider, { value: toolComponents, children: _jsx(GenerativeUIComponentsContext.Provider, { value: generativeUIComponents, children: _jsx(RenderAssistantTextContext.Provider, { value: renderAssistantText, children: _jsxs(AssistantRuntimeProvider, { runtime: runtime, children: [_jsx(ComposerPrefill, { text: initialPrompt }), _jsx("div", { style: {
401
+ height: '100%',
324
402
  display: 'flex',
325
403
  flexDirection: 'column',
326
- gap: 16,
327
- }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
328
- display: 'flex',
329
- alignItems: 'center',
330
- justifyContent: 'center',
331
- minHeight: 300,
332
- color: colors.textMuted,
333
- textAlign: 'center',
334
- fontSize: 14,
335
- }, children: emptyMessage ??
336
- (props.threadId
337
- ? 'Send a message to start the conversation.'
338
- : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
339
- UserMessage,
340
- AssistantMessage,
341
- } })] }) }), _jsx("div", { style: { maxWidth: maxWidth === 'none' ? undefined : maxWidth, margin: '0 auto', width: '100%', padding: '0 16px' }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }));
404
+ background: colors.bg,
405
+ }, children: _jsxs(ThreadPrimitive.Root, { style: {
406
+ display: 'flex',
407
+ flexDirection: 'column',
408
+ flex: 1,
409
+ minHeight: 0,
410
+ }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
411
+ flex: 1,
412
+ minHeight: 0,
413
+ overflowY: 'auto',
414
+ }, children: _jsxs("div", { style: {
415
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
416
+ margin: '0 auto',
417
+ padding: 16,
418
+ display: 'flex',
419
+ flexDirection: 'column',
420
+ gap: 16,
421
+ }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
422
+ display: 'flex',
423
+ alignItems: 'center',
424
+ justifyContent: 'center',
425
+ minHeight: 300,
426
+ color: colors.textMuted,
427
+ textAlign: 'center',
428
+ fontSize: 14,
429
+ }, children: emptyMessage ??
430
+ (props.threadId
431
+ ? 'Send a message to start the conversation.'
432
+ : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
433
+ UserMessage,
434
+ AssistantMessage,
435
+ } })] }) }), _jsx("div", { style: {
436
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
437
+ margin: '0 auto',
438
+ width: '100%',
439
+ padding: '0 16px',
440
+ }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) })] }) }) }) }) }) }) }));
342
441
  }
@@ -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: {
@@ -328,14 +375,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
328
375
  }
329
376
  const text = { value: '' };
330
377
  const toolCalls = [];
378
+ const structuredParts = [];
331
379
  let pendingContent = null;
332
380
  const yieldContent = () => {
333
- const content = buildContent(text, toolCalls);
381
+ const content = buildRichContent(text, structuredParts, toolCalls);
334
382
  if (content.length > 0)
335
383
  pendingContent = content;
336
384
  };
337
385
  const reader = response.body.getReader();
338
- const approvals = await processStream(reader, text, toolCalls, yieldContent, onFinishRef.current ?? undefined);
386
+ const approvals = await processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinishRef.current ?? undefined);
339
387
  if (approvals.length === 0) {
340
388
  // No approval needed — yield final content and done
341
389
  if (pendingContent) {
@@ -378,7 +426,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
378
426
  toolCalls.splice(i, 1);
379
427
  }
380
428
  }
381
- const content = buildContent(text, toolCalls);
429
+ const content = buildRichContent(text, structuredParts, toolCalls);
382
430
  yield {
383
431
  content,
384
432
  status: {
@@ -446,7 +494,12 @@ export const convertDbMessages = (dbMessages) => {
446
494
  continue;
447
495
  const parts = [];
448
496
  if (msg.content) {
449
- parts.push({ type: 'text', text: msg.content });
497
+ if (Array.isArray(msg.content)) {
498
+ parts.push(...msg.content);
499
+ }
500
+ else {
501
+ parts.push({ type: 'text', text: msg.content });
502
+ }
450
503
  }
451
504
  if (Array.isArray(msg.toolCalls)) {
452
505
  for (const tc of msg.toolCalls) {
@@ -533,6 +586,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
533
586
  // Resume uses SSE (same as streaming mode)
534
587
  let lastText = { value: '' };
535
588
  let lastToolCalls = [];
589
+ let lastStructuredParts = [];
536
590
  let nextApprovals = [];
537
591
  for (let i = 0; i < decisions.length; i++) {
538
592
  const decision = decisions[i];
@@ -560,17 +614,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
560
614
  }
561
615
  const text = { value: '' };
562
616
  const toolCalls = [];
617
+ const structuredParts = [];
563
618
  const reader = resumeResponse.body.getReader();
564
- const streamApprovals = await processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
619
+ const streamApprovals = await processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
565
620
  ? (onFinishRef.current ?? undefined)
566
621
  : undefined);
567
622
  lastText = text;
568
623
  lastToolCalls = toolCalls;
624
+ lastStructuredParts = structuredParts;
569
625
  if (streamApprovals.length > 0) {
570
626
  nextApprovals = streamApprovals;
571
627
  }
572
628
  }
573
- const content = buildContent(lastText, lastToolCalls);
629
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
574
630
  if (nextApprovals.length > 0) {
575
631
  pendingApprovalsRef.current = nextApprovals;
576
632
  setPendingApprovalsRef.current(nextApprovals);
@@ -596,7 +652,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
596
652
  lastToolCalls.push(approvalToolCall);
597
653
  }
598
654
  }
599
- const updatedContent = buildContent(lastText, lastToolCalls);
655
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
600
656
  yield {
601
657
  content: updatedContent,
602
658
  status: {
@@ -656,9 +712,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
656
712
  },
657
713
  }));
658
714
  const content = [];
659
- if (json.result) {
660
- content.push({ type: 'text', text: json.result });
661
- }
715
+ content.push(...buildContentFromAgentResult(json.result));
662
716
  content.push(...toolCalls);
663
717
  yield {
664
718
  content,
@@ -671,10 +725,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
671
725
  }
672
726
  // No approvals — yield complete content
673
727
  onFinishRef.current?.();
674
- const content = [];
675
- if (json.result) {
676
- content.push({ type: 'text', text: String(json.result) });
677
- }
728
+ const content = buildContentFromAgentResult(json.result);
678
729
  if (content.length > 0) {
679
730
  yield { content };
680
731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",