@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.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,44 @@
1
1
  # @pikku/assistant-ui
2
2
 
3
+ ## 0.12.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 9060165: The console now shows function version history, live queue depths with a Failed column, and scheduler last-run status with run history. Workflow canvas and run selector have been polished. The console build is ~6.5× faster thanks to a switch to rolldown-vite (Vite 7 + Oxc React transform).
8
+
9
+ ## 0.12.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 424c777: `PikkuAgentChat` now accepts a `toolComponents` prop — a map of
14
+ `toolName` → React component — for per-tool custom rendering inside
15
+ the assistant bubble. Unmatched tool calls continue to fall through to
16
+ the default expandable tool-call display.
17
+
18
+ This unlocks generative-UI patterns: register a `renderWidget` tool on
19
+ the agent, return structured props from it, and mount real UI (charts,
20
+ diffs, cards) inline in the chat from the persisted tool-call args.
21
+ Because the rendered widget is just a tool call under the hood, it
22
+ survives refresh, streams correctly, and stays part of the thread's
23
+ history.
24
+
25
+ ```tsx
26
+ <PikkuAgentChat
27
+ agentName="myAgent"
28
+ threadId={threadId}
29
+ resourceId={userId}
30
+ api="/rpc/agent"
31
+ toolComponents={{
32
+ renderWidget: ({ args }) => <WidgetRegistry spec={args} />,
33
+ }}
34
+ />
35
+ ```
36
+
3
37
  ## 0.12.3
4
38
 
5
39
  ### Patch Changes
6
40
 
7
41
  - f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
8
-
9
42
  - Unified CredentialService with lazy loading per user via pikkuUserId
10
43
  - wire.getCredential() for typed single credential lookup
11
44
  - MissingCredentialError with structured payload for client-side connect flows
@@ -1,5 +1,8 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
1
3
  import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
2
4
  export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
5
+ initialPrompt?: string;
3
6
  emptyMessage?: string;
4
7
  /** Hide tool calls from the chat display.
5
8
  * - `true`: hide all non-approval tool calls
@@ -9,5 +12,18 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
9
12
  dark?: boolean;
10
13
  /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
14
  maxWidth?: number | 'none';
15
+ /**
16
+ * Per-tool renderers. Map `toolName` → React component to replace the
17
+ * default expandable tool-call box for that tool. Enables generative-UI
18
+ * patterns: e.g. register a `renderWidget` tool on the agent and mount
19
+ * real UI (charts, diffs, cards) inline in the assistant bubble from the
20
+ * persisted tool-call args.
21
+ *
22
+ * Any tool without an entry here falls through to the default renderer
23
+ * (which still respects `hideToolCalls` and the approval-request UI).
24
+ */
25
+ toolComponents?: Record<string, ToolCallMessagePartComponent>;
26
+ renderAssistantText?: (text: string) => ReactNode;
27
+ generativeUIComponents?: Record<string, ComponentType<any>>;
12
28
  }
13
29
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -59,6 +59,9 @@ const darkColors = {
59
59
  };
60
60
  const ColorsContext = (0, react_1.createContext)(lightColors);
61
61
  const HideToolCallsContext = (0, react_1.createContext)(undefined);
62
+ const ToolComponentsContext = (0, react_1.createContext)(undefined);
63
+ const GenerativeUIComponentsContext = (0, react_1.createContext)(undefined);
64
+ const RenderAssistantTextContext = (0, react_1.createContext)(undefined);
62
65
  function shouldHideToolCall(hideToolCalls, toolName) {
63
66
  if (!hideToolCalls)
64
67
  return false;
@@ -149,7 +152,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
149
152
  padding: '2px 6px',
150
153
  borderRadius: 3,
151
154
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
152
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
155
+ color: responded === 'approved'
156
+ ? colors.successColor
157
+ : colors.errorColor,
153
158
  }, children: responded })] }));
154
159
  }
155
160
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
@@ -199,7 +204,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
199
204
  borderRadius: 4,
200
205
  overflow: 'auto',
201
206
  color: colors.text,
202
- }, children: JSON.stringify(displayArgs, null, 2) }), result !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { style: { fontSize: 12, color: colors.textMuted, marginTop: 8, marginBottom: 2 }, children: "Result:" }), (0, jsx_runtime_1.jsx)("pre", { style: {
207
+ }, children: JSON.stringify(displayArgs, null, 2) }), result !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { style: {
208
+ fontSize: 12,
209
+ color: colors.textMuted,
210
+ marginTop: 8,
211
+ marginBottom: 2,
212
+ }, children: "Result:" }), (0, jsx_runtime_1.jsx)("pre", { style: {
203
213
  fontSize: 11,
204
214
  background: colors.codeBg,
205
215
  padding: 8,
@@ -210,25 +220,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
210
220
  ? result
211
221
  : JSON.stringify(result, null, 2) })] }))] }))] }));
212
222
  };
213
- const MarkdownText = ({ text, colors }) => {
223
+ const MarkdownText = ({ text, colors, }) => {
214
224
  const components = (0, react_1.useMemo)(() => ({
215
- p: ({ children }) => ((0, jsx_runtime_1.jsx)("p", { style: { margin: '0 0 8px', fontSize: 14, lineHeight: 1.6, color: colors.text }, children: children })),
225
+ p: ({ children }) => ((0, jsx_runtime_1.jsx)("p", { style: {
226
+ margin: '0 0 8px',
227
+ fontSize: 14,
228
+ lineHeight: 1.6,
229
+ color: colors.text,
230
+ }, children: children })),
216
231
  strong: ({ children }) => ((0, jsx_runtime_1.jsx)("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
217
232
  em: ({ children }) => ((0, jsx_runtime_1.jsx)("em", { style: { color: colors.text }, children: children })),
218
- ul: ({ children }) => ((0, jsx_runtime_1.jsx)("ul", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
219
- ol: ({ children }) => ((0, jsx_runtime_1.jsx)("ol", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
233
+ ul: ({ children }) => ((0, jsx_runtime_1.jsx)("ul", { style: {
234
+ margin: '4px 0 8px',
235
+ paddingLeft: 20,
236
+ fontSize: 14,
237
+ color: colors.text,
238
+ }, children: children })),
239
+ ol: ({ children }) => ((0, jsx_runtime_1.jsx)("ol", { style: {
240
+ margin: '4px 0 8px',
241
+ paddingLeft: 20,
242
+ fontSize: 14,
243
+ color: colors.text,
244
+ }, children: children })),
220
245
  li: ({ children }) => ((0, jsx_runtime_1.jsx)("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
221
246
  code: ({ children, className }) => {
222
247
  const isBlock = className === null || className === void 0 ? void 0 : className.startsWith('language-');
223
248
  if (isBlock) {
224
- return ((0, jsx_runtime_1.jsx)("pre", { style: { background: colors.codeBg, padding: 10, borderRadius: 4, overflow: 'auto', margin: '4px 0 8px', fontSize: 12 }, children: (0, jsx_runtime_1.jsx)("code", { style: { color: colors.text }, children: children }) }));
249
+ return ((0, jsx_runtime_1.jsx)("pre", { style: {
250
+ background: colors.codeBg,
251
+ padding: 10,
252
+ borderRadius: 4,
253
+ overflow: 'auto',
254
+ margin: '4px 0 8px',
255
+ fontSize: 12,
256
+ }, children: (0, jsx_runtime_1.jsx)("code", { style: { color: colors.text }, children: children }) }));
225
257
  }
226
- return ((0, jsx_runtime_1.jsx)("code", { style: { background: colors.codeBg, padding: '1px 4px', borderRadius: 3, fontSize: 13, color: colors.text }, children: children }));
258
+ return ((0, jsx_runtime_1.jsx)("code", { style: {
259
+ background: colors.codeBg,
260
+ padding: '1px 4px',
261
+ borderRadius: 3,
262
+ fontSize: 13,
263
+ color: colors.text,
264
+ }, children: children }));
227
265
  },
228
266
  pre: ({ children }) => (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children }),
229
- h1: ({ children }) => (0, jsx_runtime_1.jsx)("h3", { style: { margin: '8px 0 4px', fontSize: 16, fontWeight: 600, color: colors.text }, children: children }),
230
- h2: ({ children }) => (0, jsx_runtime_1.jsx)("h4", { style: { margin: '8px 0 4px', fontSize: 15, fontWeight: 600, color: colors.text }, children: children }),
231
- h3: ({ children }) => (0, jsx_runtime_1.jsx)("h5", { style: { margin: '8px 0 4px', fontSize: 14, fontWeight: 600, color: colors.text }, children: children }),
267
+ h1: ({ children }) => ((0, jsx_runtime_1.jsx)("h3", { style: {
268
+ margin: '8px 0 4px',
269
+ fontSize: 16,
270
+ fontWeight: 600,
271
+ color: colors.text,
272
+ }, children: children })),
273
+ h2: ({ children }) => ((0, jsx_runtime_1.jsx)("h4", { style: {
274
+ margin: '8px 0 4px',
275
+ fontSize: 15,
276
+ fontWeight: 600,
277
+ color: colors.text,
278
+ }, children: children })),
279
+ h3: ({ children }) => ((0, jsx_runtime_1.jsx)("h5", { style: {
280
+ margin: '8px 0 4px',
281
+ fontSize: 14,
282
+ fontWeight: 600,
283
+ color: colors.text,
284
+ }, children: children })),
232
285
  a: ({ href, children }) => ((0, jsx_runtime_1.jsx)("a", { href: href, target: "_blank", rel: "noopener noreferrer", style: { color: colors.textMuted, textDecoration: 'underline' }, children: children })),
233
286
  }), [colors]);
234
287
  return (0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: components, children: text });
@@ -249,11 +302,18 @@ const UserMessage = () => {
249
302
  borderRadius: 12,
250
303
  backgroundColor: colors.userBubble,
251
304
  }, children: (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: {
252
- Text: ({ text }) => ((0, jsx_runtime_1.jsx)("span", { style: { fontSize: 14, whiteSpace: 'pre-wrap', color: colors.text }, children: text })),
305
+ Text: ({ text }) => ((0, jsx_runtime_1.jsx)("span", { style: {
306
+ fontSize: 14,
307
+ whiteSpace: 'pre-wrap',
308
+ color: colors.text,
309
+ }, children: text })),
253
310
  } }) })] }) }));
254
311
  };
255
312
  const AssistantMessage = () => {
256
313
  const colors = (0, react_1.useContext)(ColorsContext);
314
+ const toolComponents = (0, react_1.useContext)(ToolComponentsContext);
315
+ const generativeUIComponents = (0, react_1.useContext)(GenerativeUIComponentsContext);
316
+ const renderAssistantText = (0, react_1.useContext)(RenderAssistantTextContext);
257
317
  return ((0, jsx_runtime_1.jsx)("div", { style: {
258
318
  display: 'flex',
259
319
  justifyContent: 'flex-start',
@@ -262,12 +322,16 @@ const AssistantMessage = () => {
262
322
  padding: 12,
263
323
  borderRadius: 12,
264
324
  backgroundColor: colors.assistantBubble,
265
- }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: {
266
- Text: ({ text }) => ((0, jsx_runtime_1.jsx)(MarkdownText, { text: text, colors: colors })),
267
- tools: {
325
+ }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: Object.assign({ Text: ({ text }) => renderAssistantText ? ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: renderAssistantText(text) })) : ((0, jsx_runtime_1.jsx)(MarkdownText, { text: text, colors: colors })), tools: {
326
+ by_name: toolComponents,
268
327
  Fallback: (props) => ((0, jsx_runtime_1.jsx)(ToolCallDisplay, { toolCallId: props.toolCallId, toolName: props.toolName, args: props.args, result: props.result, status: (0, use_pikku_agent_runtime_js_1.resolvePikkuToolStatus)(props.status, props.result), addResult: props.addResult })),
269
- },
270
- } }), (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { last: true, children: (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.If, { running: true, children: (0, jsx_runtime_1.jsx)("div", { style: {
328
+ } }, (generativeUIComponents
329
+ ? {
330
+ generativeUI: {
331
+ components: generativeUIComponents,
332
+ },
333
+ }
334
+ : {})) }), (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { last: true, children: (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.If, { running: true, children: (0, jsx_runtime_1.jsx)("div", { style: {
271
335
  display: 'flex',
272
336
  alignItems: 'center',
273
337
  gap: 6,
@@ -276,71 +340,99 @@ const AssistantMessage = () => {
276
340
  color: colors.textMuted,
277
341
  }, children: "Thinking..." }) }) })] })] }) }));
278
342
  };
343
+ const ComposerPrefill = ({ text }) => {
344
+ const composer = (0, react_2.useComposerRuntime)();
345
+ const filled = (0, react_1.useRef)(false);
346
+ (0, react_1.useEffect)(() => {
347
+ if (filled.current || !text)
348
+ return;
349
+ filled.current = true;
350
+ if (composer.getState().text === '')
351
+ composer.setText(text);
352
+ }, [text, composer]);
353
+ return null;
354
+ };
279
355
  const PikkuComposer = ({ disabled, }) => {
280
356
  const colors = (0, react_1.useContext)(ColorsContext);
281
- return ((0, jsx_runtime_1.jsx)("div", { style: { padding: '8px 0 16px' }, children: (0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Root, { children: (0, jsx_runtime_1.jsxs)("div", { style: Object.assign({ border: `1px solid ${colors.border}`, borderRadius: 12, overflow: 'hidden', display: 'flex', alignItems: 'flex-end', padding: '6px 12px', gap: 8 }, (disabled ? { opacity: 0.5, pointerEvents: 'none' } : {})), children: [(0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
282
- flex: 1,
357
+ return ((0, jsx_runtime_1.jsx)("div", { style: { padding: '8px 0 16px' }, children: (0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Root, { children: (0, jsx_runtime_1.jsxs)("div", { style: Object.assign({ position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', gap: 10, backgroundColor: colors.assistantBubble, border: `1px solid ${colors.border}`, borderRadius: 24, padding: '14px 12px 10px', 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)' }, (disabled
360
+ ? { opacity: 0.5, pointerEvents: 'none' }
361
+ : {})), children: [(0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 1, disabled: disabled !== null && disabled !== void 0 ? disabled : false, style: {
362
+ width: '100%',
363
+ background: 'transparent',
283
364
  border: 'none',
284
365
  outline: 'none',
285
- resize: 'none',
366
+ color: colors.text,
286
367
  fontSize: 14,
287
368
  fontFamily: 'inherit',
288
- padding: '4px 0',
289
- background: colors.inputBg,
290
- color: colors.text,
291
- } }), (0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Send, { disabled: disabled, style: {
292
- width: 28,
293
- height: 28,
294
- borderRadius: '50%',
295
- border: 'none',
296
- background: disabled ? colors.textMuted : colors.sendBg,
297
- color: colors.sendColor,
298
- cursor: disabled ? 'not-allowed' : 'pointer',
369
+ resize: 'none',
370
+ lineHeight: 1.5,
371
+ overflowY: 'auto',
372
+ } }), (0, jsx_runtime_1.jsx)("div", { style: {
373
+ width: '100%',
299
374
  display: 'flex',
300
375
  alignItems: 'center',
301
- justifyContent: 'center',
302
- flexShrink: 0,
303
- marginBottom: 2,
304
- fontSize: 14,
305
- }, children: "\u25B6" })] }) }) }));
376
+ gap: 6,
377
+ minWidth: 0,
378
+ justifyContent: 'flex-end',
379
+ }, children: (0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Send, { disabled: disabled !== null && disabled !== void 0 ? disabled : false, style: {
380
+ width: 30,
381
+ height: 30,
382
+ borderRadius: 999,
383
+ border: `1px solid ${colors.border}`,
384
+ background: '#e8e8e8',
385
+ color: '#111111',
386
+ cursor: disabled ? 'not-allowed' : 'pointer',
387
+ display: 'flex',
388
+ alignItems: 'center',
389
+ justifyContent: 'center',
390
+ flexShrink: 0,
391
+ transition: 'background-color 150ms ease, color 150ms ease, border-color 150ms ease',
392
+ }, children: "\u2191" }) })] }) }) }));
306
393
  };
307
394
  function PikkuAgentChat(props) {
308
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768 } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth"]);
395
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, renderAssistantText, generativeUIComponents, initialPrompt } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth", "toolComponents", "renderAssistantText", "generativeUIComponents", "initialPrompt"]);
309
396
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = (0, use_pikku_agent_runtime_js_1.usePikkuAgentRuntime)(runtimeOptions);
310
397
  const colors = dark ? darkColors : lightColors;
311
- return ((0, jsx_runtime_1.jsx)(ColorsContext.Provider, { value: colors, children: (0, jsx_runtime_1.jsx)(use_pikku_agent_runtime_js_1.PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: (0, jsx_runtime_1.jsx)(HideToolCallsContext.Provider, { value: hideToolCalls, children: (0, jsx_runtime_1.jsx)(react_2.AssistantRuntimeProvider, { runtime: runtime, children: (0, jsx_runtime_1.jsx)("div", { style: {
312
- height: '100%',
313
- display: 'flex',
314
- flexDirection: 'column',
315
- background: colors.bg,
316
- }, children: (0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Root, { style: {
317
- display: 'flex',
318
- flexDirection: 'column',
319
- flex: 1,
320
- minHeight: 0,
321
- }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Viewport, { style: {
322
- flex: 1,
323
- minHeight: 0,
324
- overflowY: 'auto',
325
- }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
326
- maxWidth: maxWidth === 'none' ? undefined : maxWidth,
327
- margin: '0 auto',
328
- padding: 16,
398
+ return ((0, jsx_runtime_1.jsx)(ColorsContext.Provider, { value: colors, children: (0, jsx_runtime_1.jsx)(use_pikku_agent_runtime_js_1.PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: (0, jsx_runtime_1.jsx)(HideToolCallsContext.Provider, { value: hideToolCalls, children: (0, jsx_runtime_1.jsx)(ToolComponentsContext.Provider, { value: toolComponents, children: (0, jsx_runtime_1.jsx)(GenerativeUIComponentsContext.Provider, { value: generativeUIComponents, children: (0, jsx_runtime_1.jsx)(RenderAssistantTextContext.Provider, { value: renderAssistantText, children: (0, jsx_runtime_1.jsxs)(react_2.AssistantRuntimeProvider, { runtime: runtime, children: [(0, jsx_runtime_1.jsx)(ComposerPrefill, { text: initialPrompt }), (0, jsx_runtime_1.jsx)("div", { style: {
399
+ height: '100%',
329
400
  display: 'flex',
330
401
  flexDirection: 'column',
331
- gap: 16,
332
- }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Empty, { children: (0, jsx_runtime_1.jsx)("div", { style: {
333
- display: 'flex',
334
- alignItems: 'center',
335
- justifyContent: 'center',
336
- minHeight: 300,
337
- color: colors.textMuted,
338
- textAlign: 'center',
339
- fontSize: 14,
340
- }, children: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : (props.threadId
341
- ? 'Send a message to start the conversation.'
342
- : 'Start a new conversation.') }) }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
343
- UserMessage,
344
- AssistantMessage,
345
- } })] }) }), (0, jsx_runtime_1.jsx)("div", { style: { maxWidth: maxWidth === 'none' ? undefined : maxWidth, margin: '0 auto', width: '100%', padding: '0 16px' }, children: (0, jsx_runtime_1.jsx)(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }));
402
+ background: colors.bg,
403
+ }, children: (0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Root, { style: {
404
+ display: 'flex',
405
+ flexDirection: 'column',
406
+ flex: 1,
407
+ minHeight: 0,
408
+ }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Viewport, { style: {
409
+ flex: 1,
410
+ minHeight: 0,
411
+ overflowY: 'auto',
412
+ }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
413
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
414
+ margin: '0 auto',
415
+ padding: 16,
416
+ display: 'flex',
417
+ flexDirection: 'column',
418
+ gap: 16,
419
+ }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Empty, { children: (0, jsx_runtime_1.jsx)("div", { style: {
420
+ display: 'flex',
421
+ alignItems: 'center',
422
+ justifyContent: 'center',
423
+ minHeight: 300,
424
+ color: colors.textMuted,
425
+ textAlign: 'center',
426
+ fontSize: 14,
427
+ }, children: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : (props.threadId
428
+ ? 'Send a message to start the conversation.'
429
+ : 'Start a new conversation.') }) }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
430
+ UserMessage,
431
+ AssistantMessage,
432
+ } })] }) }), (0, jsx_runtime_1.jsx)("div", { style: {
433
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
434
+ margin: '0 auto',
435
+ width: '100%',
436
+ padding: '0 16px',
437
+ }, children: (0, jsx_runtime_1.jsx)(PikkuComposer, { disabled: isAwaitingApproval }) })] }) })] }) }) }) }) }) }) }));
346
438
  }
@@ -73,7 +73,7 @@ function parseSSEStream(reader) {
73
73
  * Shared helper: consume an SSE stream and populate text/toolCalls.
74
74
  * Returns an array of PendingApprovals when the stream requests them, or empty when done.
75
75
  */
76
- function processStream(reader, text, toolCalls, yieldContent, onFinish) {
76
+ function processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinish) {
77
77
  return __awaiter(this, void 0, void 0, function* () {
78
78
  var _a, e_1, _b, _c;
79
79
  const pendingApprovals = [];
@@ -122,6 +122,31 @@ function processStream(reader, text, toolCalls, yieldContent, onFinish) {
122
122
  }
123
123
  break;
124
124
  }
125
+ case 'generative-ui': {
126
+ const nextPart = {
127
+ type: 'generative-ui',
128
+ spec: event.spec,
129
+ };
130
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'generative-ui');
131
+ if (existingIndex === -1)
132
+ structuredParts.push(nextPart);
133
+ else
134
+ structuredParts[existingIndex] = nextPart;
135
+ break;
136
+ }
137
+ case 'data': {
138
+ const nextPart = {
139
+ type: 'data',
140
+ name: event.name,
141
+ data: event.data,
142
+ };
143
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'data' && part.name === event.name);
144
+ if (existingIndex === -1)
145
+ structuredParts.push(nextPart);
146
+ else
147
+ structuredParts[existingIndex] = nextPart;
148
+ break;
149
+ }
125
150
  case 'approval-request':
126
151
  pendingApprovals.push({
127
152
  toolCallId: event.toolCallId,
@@ -205,13 +230,32 @@ function resolvePikkuToolStatus(status, result) {
205
230
  return { type: 'error' };
206
231
  return { type: 'completed' };
207
232
  }
208
- function buildContent(text, toolCalls) {
233
+ function buildRichContent(text, structuredParts, toolCalls) {
209
234
  const content = [];
210
235
  if (text.value)
211
236
  content.push({ type: 'text', text: text.value });
237
+ content.push(...structuredParts);
212
238
  content.push(...toolCalls);
213
239
  return content;
214
240
  }
241
+ function buildContentFromAgentResult(result) {
242
+ const content = [];
243
+ if (typeof result === 'string') {
244
+ if (result)
245
+ content.push({ type: 'text', text: result });
246
+ return content;
247
+ }
248
+ if (!result || typeof result !== 'object')
249
+ return content;
250
+ const record = result;
251
+ if (typeof record.text === 'string' && record.text) {
252
+ content.push({ type: 'text', text: record.text });
253
+ }
254
+ if (record.ui != null) {
255
+ content.push({ type: 'generative-ui', spec: record.ui });
256
+ }
257
+ return content;
258
+ }
215
259
  function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
216
260
  return {
217
261
  run(_a) {
@@ -237,6 +281,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
237
281
  // The last resume triggers continuation (next LLM step).
238
282
  let lastText = { value: '' };
239
283
  let lastToolCalls = [];
284
+ let lastStructuredParts = [];
240
285
  let nextApprovals = [];
241
286
  for (let i = 0; i < decisions.length; i++) {
242
287
  const decision = decisions[i];
@@ -262,8 +307,9 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
262
307
  }
263
308
  const text = { value: '' };
264
309
  const toolCalls = [];
310
+ const structuredParts = [];
265
311
  const reader = resumeResponse.body.getReader();
266
- const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
312
+ const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
267
313
  ? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
268
314
  : undefined)
269
315
  // Keep the last resume's output (it has continuation content)
@@ -271,12 +317,13 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
271
317
  // Keep the last resume's output (it has continuation content)
272
318
  lastText = text;
273
319
  lastToolCalls = toolCalls;
320
+ lastStructuredParts = structuredParts;
274
321
  if (streamApprovals.length > 0) {
275
322
  nextApprovals = streamApprovals;
276
323
  }
277
324
  }
278
325
  // Build content from the last resume's output
279
- const content = buildContent(lastText, lastToolCalls);
326
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
280
327
  if (nextApprovals.length > 0) {
281
328
  // More approvals from continuation — show them
282
329
  pendingApprovalsRef.current = nextApprovals;
@@ -301,7 +348,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
301
348
  lastToolCalls.push(approvalToolCall);
302
349
  }
303
350
  }
304
- const updatedContent = buildContent(lastText, lastToolCalls);
351
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
305
352
  yield yield __await({
306
353
  content: updatedContent,
307
354
  status: {
@@ -380,14 +427,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
380
427
  }
381
428
  const text = { value: '' };
382
429
  const toolCalls = [];
430
+ const structuredParts = [];
383
431
  let pendingContent = null;
384
432
  const yieldContent = () => {
385
- const content = buildContent(text, toolCalls);
433
+ const content = buildRichContent(text, structuredParts, toolCalls);
386
434
  if (content.length > 0)
387
435
  pendingContent = content;
388
436
  };
389
437
  const reader = response.body.getReader();
390
- const approvals = yield __await(processStream(reader, text, toolCalls, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
438
+ const approvals = yield __await(processStream(reader, text, toolCalls, structuredParts, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
391
439
  if (approvals.length === 0) {
392
440
  // No approval needed — yield final content and done
393
441
  if (pendingContent) {
@@ -427,7 +475,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
427
475
  toolCalls.splice(i, 1);
428
476
  }
429
477
  }
430
- const content = buildContent(text, toolCalls);
478
+ const content = buildRichContent(text, structuredParts, toolCalls);
431
479
  yield yield __await({
432
480
  content,
433
481
  status: {
@@ -497,7 +545,12 @@ const convertDbMessages = (dbMessages) => {
497
545
  continue;
498
546
  const parts = [];
499
547
  if (msg.content) {
500
- parts.push({ type: 'text', text: msg.content });
548
+ if (Array.isArray(msg.content)) {
549
+ parts.push(...msg.content);
550
+ }
551
+ else {
552
+ parts.push({ type: 'text', text: msg.content });
553
+ }
501
554
  }
502
555
  if (Array.isArray(msg.toolCalls)) {
503
556
  for (const tc of msg.toolCalls) {
@@ -587,6 +640,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
587
640
  // Resume uses SSE (same as streaming mode)
588
641
  let lastText = { value: '' };
589
642
  let lastToolCalls = [];
643
+ let lastStructuredParts = [];
590
644
  let nextApprovals = [];
591
645
  for (let i = 0; i < decisions.length; i++) {
592
646
  const decision = decisions[i];
@@ -611,17 +665,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
611
665
  }
612
666
  const text = { value: '' };
613
667
  const toolCalls = [];
668
+ const structuredParts = [];
614
669
  const reader = resumeResponse.body.getReader();
615
- const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
670
+ const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
616
671
  ? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
617
672
  : undefined));
618
673
  lastText = text;
619
674
  lastToolCalls = toolCalls;
675
+ lastStructuredParts = structuredParts;
620
676
  if (streamApprovals.length > 0) {
621
677
  nextApprovals = streamApprovals;
622
678
  }
623
679
  }
624
- const content = buildContent(lastText, lastToolCalls);
680
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
625
681
  if (nextApprovals.length > 0) {
626
682
  pendingApprovalsRef.current = nextApprovals;
627
683
  setPendingApprovalsRef.current(nextApprovals);
@@ -644,7 +700,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
644
700
  lastToolCalls.push(approvalToolCall);
645
701
  }
646
702
  }
647
- const updatedContent = buildContent(lastText, lastToolCalls);
703
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
648
704
  yield yield __await({
649
705
  content: updatedContent,
650
706
  status: {
@@ -701,9 +757,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
701
757
  : {})), (approval.reason ? { __approvalReason: approval.reason } : {})),
702
758
  }));
703
759
  const content = [];
704
- if (json.result) {
705
- content.push({ type: 'text', text: json.result });
706
- }
760
+ content.push(...buildContentFromAgentResult(json.result));
707
761
  content.push(...toolCalls);
708
762
  yield yield __await({
709
763
  content,
@@ -716,10 +770,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
716
770
  }
717
771
  // No approvals — yield complete content
718
772
  (_f = onFinishRef.current) === null || _f === void 0 ? void 0 : _f.call(onFinishRef);
719
- const content = [];
720
- if (json.result) {
721
- content.push({ type: 'text', text: String(json.result) });
722
- }
773
+ const content = buildContentFromAgentResult(json.result);
723
774
  if (content.length > 0) {
724
775
  yield yield __await({ content });
725
776
  }
@@ -1,5 +1,8 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
1
3
  import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
2
4
  export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
5
+ initialPrompt?: string;
3
6
  emptyMessage?: string;
4
7
  /** Hide tool calls from the chat display.
5
8
  * - `true`: hide all non-approval tool calls
@@ -9,5 +12,18 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
9
12
  dark?: boolean;
10
13
  /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
14
  maxWidth?: number | 'none';
15
+ /**
16
+ * Per-tool renderers. Map `toolName` → React component to replace the
17
+ * default expandable tool-call box for that tool. Enables generative-UI
18
+ * patterns: e.g. register a `renderWidget` tool on the agent and mount
19
+ * real UI (charts, diffs, cards) inline in the assistant bubble from the
20
+ * persisted tool-call args.
21
+ *
22
+ * Any tool without an entry here falls through to the default renderer
23
+ * (which still respects `hideToolCalls` and the approval-request UI).
24
+ */
25
+ toolComponents?: Record<string, ToolCallMessagePartComponent>;
26
+ renderAssistantText?: (text: string) => ReactNode;
27
+ generativeUIComponents?: Record<string, ComponentType<any>>;
12
28
  }
13
29
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;