@pikku/assistant-ui 0.12.3 → 0.12.4

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,5 +1,33 @@
1
1
  # @pikku/assistant-ui
2
2
 
3
+ ## 0.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 424c777: `PikkuAgentChat` now accepts a `toolComponents` prop — a map of
8
+ `toolName` → React component — for per-tool custom rendering inside
9
+ the assistant bubble. Unmatched tool calls continue to fall through to
10
+ the default expandable tool-call display.
11
+
12
+ This unlocks generative-UI patterns: register a `renderWidget` tool on
13
+ the agent, return structured props from it, and mount real UI (charts,
14
+ diffs, cards) inline in the chat from the persisted tool-call args.
15
+ Because the rendered widget is just a tool call under the hood, it
16
+ survives refresh, streams correctly, and stays part of the thread's
17
+ history.
18
+
19
+ ```tsx
20
+ <PikkuAgentChat
21
+ agentName="myAgent"
22
+ threadId={threadId}
23
+ resourceId={userId}
24
+ api="/rpc/agent"
25
+ toolComponents={{
26
+ renderWidget: ({ args }) => <WidgetRegistry spec={args} />,
27
+ }}
28
+ />
29
+ ```
30
+
3
31
  ## 0.12.3
4
32
 
5
33
  ### Patch Changes
@@ -1,3 +1,4 @@
1
+ import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
1
2
  import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
2
3
  export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
3
4
  emptyMessage?: string;
@@ -9,5 +10,16 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
9
10
  dark?: boolean;
10
11
  /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
12
  maxWidth?: number | 'none';
13
+ /**
14
+ * Per-tool renderers. Map `toolName` → React component to replace the
15
+ * default expandable tool-call box for that tool. Enables generative-UI
16
+ * patterns: e.g. register a `renderWidget` tool on the agent and mount
17
+ * real UI (charts, diffs, cards) inline in the assistant bubble from the
18
+ * persisted tool-call args.
19
+ *
20
+ * Any tool without an entry here falls through to the default renderer
21
+ * (which still respects `hideToolCalls` and the approval-request UI).
22
+ */
23
+ toolComponents?: Record<string, ToolCallMessagePartComponent>;
12
24
  }
13
25
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -59,6 +59,7 @@ 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);
62
63
  function shouldHideToolCall(hideToolCalls, toolName) {
63
64
  if (!hideToolCalls)
64
65
  return false;
@@ -149,7 +150,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
149
150
  padding: '2px 6px',
150
151
  borderRadius: 3,
151
152
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
152
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
153
+ color: responded === 'approved'
154
+ ? colors.successColor
155
+ : colors.errorColor,
153
156
  }, children: responded })] }));
154
157
  }
155
158
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
@@ -199,7 +202,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
199
202
  borderRadius: 4,
200
203
  overflow: 'auto',
201
204
  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: {
205
+ }, 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: {
206
+ fontSize: 12,
207
+ color: colors.textMuted,
208
+ marginTop: 8,
209
+ marginBottom: 2,
210
+ }, children: "Result:" }), (0, jsx_runtime_1.jsx)("pre", { style: {
203
211
  fontSize: 11,
204
212
  background: colors.codeBg,
205
213
  padding: 8,
@@ -210,25 +218,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
210
218
  ? result
211
219
  : JSON.stringify(result, null, 2) })] }))] }))] }));
212
220
  };
213
- const MarkdownText = ({ text, colors }) => {
221
+ const MarkdownText = ({ text, colors, }) => {
214
222
  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 })),
223
+ p: ({ children }) => ((0, jsx_runtime_1.jsx)("p", { style: {
224
+ margin: '0 0 8px',
225
+ fontSize: 14,
226
+ lineHeight: 1.6,
227
+ color: colors.text,
228
+ }, children: children })),
216
229
  strong: ({ children }) => ((0, jsx_runtime_1.jsx)("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
217
230
  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 })),
231
+ ul: ({ children }) => ((0, jsx_runtime_1.jsx)("ul", { style: {
232
+ margin: '4px 0 8px',
233
+ paddingLeft: 20,
234
+ fontSize: 14,
235
+ color: colors.text,
236
+ }, children: children })),
237
+ ol: ({ children }) => ((0, jsx_runtime_1.jsx)("ol", { style: {
238
+ margin: '4px 0 8px',
239
+ paddingLeft: 20,
240
+ fontSize: 14,
241
+ color: colors.text,
242
+ }, children: children })),
220
243
  li: ({ children }) => ((0, jsx_runtime_1.jsx)("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
221
244
  code: ({ children, className }) => {
222
245
  const isBlock = className === null || className === void 0 ? void 0 : className.startsWith('language-');
223
246
  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 }) }));
247
+ return ((0, jsx_runtime_1.jsx)("pre", { style: {
248
+ background: colors.codeBg,
249
+ padding: 10,
250
+ borderRadius: 4,
251
+ overflow: 'auto',
252
+ margin: '4px 0 8px',
253
+ fontSize: 12,
254
+ }, children: (0, jsx_runtime_1.jsx)("code", { style: { color: colors.text }, children: children }) }));
225
255
  }
226
- return ((0, jsx_runtime_1.jsx)("code", { style: { background: colors.codeBg, padding: '1px 4px', borderRadius: 3, fontSize: 13, color: colors.text }, children: children }));
256
+ return ((0, jsx_runtime_1.jsx)("code", { style: {
257
+ background: colors.codeBg,
258
+ padding: '1px 4px',
259
+ borderRadius: 3,
260
+ fontSize: 13,
261
+ color: colors.text,
262
+ }, children: children }));
227
263
  },
228
264
  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 }),
265
+ h1: ({ children }) => ((0, jsx_runtime_1.jsx)("h3", { style: {
266
+ margin: '8px 0 4px',
267
+ fontSize: 16,
268
+ fontWeight: 600,
269
+ color: colors.text,
270
+ }, children: children })),
271
+ h2: ({ children }) => ((0, jsx_runtime_1.jsx)("h4", { style: {
272
+ margin: '8px 0 4px',
273
+ fontSize: 15,
274
+ fontWeight: 600,
275
+ color: colors.text,
276
+ }, children: children })),
277
+ h3: ({ children }) => ((0, jsx_runtime_1.jsx)("h5", { style: {
278
+ margin: '8px 0 4px',
279
+ fontSize: 14,
280
+ fontWeight: 600,
281
+ color: colors.text,
282
+ }, children: children })),
232
283
  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
284
  }), [colors]);
234
285
  return (0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: components, children: text });
@@ -249,11 +300,16 @@ const UserMessage = () => {
249
300
  borderRadius: 12,
250
301
  backgroundColor: colors.userBubble,
251
302
  }, 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 })),
303
+ Text: ({ text }) => ((0, jsx_runtime_1.jsx)("span", { style: {
304
+ fontSize: 14,
305
+ whiteSpace: 'pre-wrap',
306
+ color: colors.text,
307
+ }, children: text })),
253
308
  } }) })] }) }));
254
309
  };
255
310
  const AssistantMessage = () => {
256
311
  const colors = (0, react_1.useContext)(ColorsContext);
312
+ const toolComponents = (0, react_1.useContext)(ToolComponentsContext);
257
313
  return ((0, jsx_runtime_1.jsx)("div", { style: {
258
314
  display: 'flex',
259
315
  justifyContent: 'flex-start',
@@ -263,8 +319,9 @@ const AssistantMessage = () => {
263
319
  borderRadius: 12,
264
320
  backgroundColor: colors.assistantBubble,
265
321
  }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: {
266
- Text: ({ text }) => ((0, jsx_runtime_1.jsx)(MarkdownText, { text: text, colors: colors })),
322
+ Text: ({ text }) => (0, jsx_runtime_1.jsx)(MarkdownText, { text: text, colors: colors }),
267
323
  tools: {
324
+ by_name: toolComponents,
268
325
  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
326
  },
270
327
  } }), (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: {
@@ -278,7 +335,9 @@ const AssistantMessage = () => {
278
335
  };
279
336
  const PikkuComposer = ({ disabled, }) => {
280
337
  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: {
338
+ 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
339
+ ? { opacity: 0.5, pointerEvents: 'none' }
340
+ : {})), children: [(0, jsx_runtime_1.jsx)(react_2.ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
282
341
  flex: 1,
283
342
  border: 'none',
284
343
  outline: 'none',
@@ -305,42 +364,47 @@ const PikkuComposer = ({ disabled, }) => {
305
364
  }, children: "\u25B6" })] }) }) }));
306
365
  };
307
366
  function PikkuAgentChat(props) {
308
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768 } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth"]);
367
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth", "toolComponents"]);
309
368
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = (0, use_pikku_agent_runtime_js_1.usePikkuAgentRuntime)(runtimeOptions);
310
369
  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: {
370
+ 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)(react_2.AssistantRuntimeProvider, { runtime: runtime, children: (0, jsx_runtime_1.jsx)("div", { style: {
371
+ height: '100%',
317
372
  display: 'flex',
318
373
  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: {
374
+ background: colors.bg,
375
+ }, children: (0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Root, { style: {
376
+ display: 'flex',
377
+ flexDirection: 'column',
378
+ flex: 1,
379
+ minHeight: 0,
380
+ }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Viewport, { style: {
381
+ flex: 1,
382
+ minHeight: 0,
383
+ overflowY: 'auto',
384
+ }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
385
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
386
+ margin: '0 auto',
387
+ padding: 16,
388
+ display: 'flex',
389
+ flexDirection: 'column',
390
+ gap: 16,
391
+ }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Empty, { children: (0, jsx_runtime_1.jsx)("div", { style: {
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ justifyContent: 'center',
395
+ minHeight: 300,
396
+ color: colors.textMuted,
397
+ textAlign: 'center',
398
+ fontSize: 14,
399
+ }, children: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : (props.threadId
400
+ ? 'Send a message to start the conversation.'
401
+ : 'Start a new conversation.') }) }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
402
+ UserMessage,
403
+ AssistantMessage,
404
+ } })] }) }), (0, jsx_runtime_1.jsx)("div", { style: {
326
405
  maxWidth: maxWidth === 'none' ? undefined : maxWidth,
327
406
  margin: '0 auto',
328
- padding: 16,
329
- display: 'flex',
330
- 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 }) })] }) }) }) }) }) }));
407
+ width: '100%',
408
+ padding: '0 16px',
409
+ }, children: (0, jsx_runtime_1.jsx)(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }) }));
346
410
  }
@@ -1,3 +1,4 @@
1
+ import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
1
2
  import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
2
3
  export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
3
4
  emptyMessage?: string;
@@ -9,5 +10,16 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
9
10
  dark?: boolean;
10
11
  /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
12
  maxWidth?: number | 'none';
13
+ /**
14
+ * Per-tool renderers. Map `toolName` → React component to replace the
15
+ * default expandable tool-call box for that tool. Enables generative-UI
16
+ * patterns: e.g. register a `renderWidget` tool on the agent and mount
17
+ * real UI (charts, diffs, cards) inline in the assistant bubble from the
18
+ * persisted tool-call args.
19
+ *
20
+ * Any tool without an entry here falls through to the default renderer
21
+ * (which still respects `hideToolCalls` and the approval-request UI).
22
+ */
23
+ toolComponents?: Record<string, ToolCallMessagePartComponent>;
12
24
  }
13
25
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
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, } from 'react';
3
3
  import Markdown from 'react-markdown';
4
4
  import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, } from '@assistant-ui/react';
5
5
  import { usePikkuAgentRuntime, PikkuApprovalContext, usePikkuApproval, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
@@ -45,6 +45,7 @@ const darkColors = {
45
45
  };
46
46
  const ColorsContext = createContext(lightColors);
47
47
  const HideToolCallsContext = createContext(undefined);
48
+ const ToolComponentsContext = createContext(undefined);
48
49
  function shouldHideToolCall(hideToolCalls, toolName) {
49
50
  if (!hideToolCalls)
50
51
  return false;
@@ -135,7 +136,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
135
136
  padding: '2px 6px',
136
137
  borderRadius: 3,
137
138
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
138
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
139
+ color: responded === 'approved'
140
+ ? colors.successColor
141
+ : colors.errorColor,
139
142
  }, children: responded })] }));
140
143
  }
141
144
  return (_jsxs("div", { style: {
@@ -185,7 +188,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
185
188
  borderRadius: 4,
186
189
  overflow: 'auto',
187
190
  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: {
191
+ }, children: JSON.stringify(displayArgs, null, 2) }), result !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
192
+ fontSize: 12,
193
+ color: colors.textMuted,
194
+ marginTop: 8,
195
+ marginBottom: 2,
196
+ }, children: "Result:" }), _jsx("pre", { style: {
189
197
  fontSize: 11,
190
198
  background: colors.codeBg,
191
199
  padding: 8,
@@ -196,25 +204,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
196
204
  ? result
197
205
  : JSON.stringify(result, null, 2) })] }))] }))] }));
198
206
  };
199
- const MarkdownText = ({ text, colors }) => {
207
+ const MarkdownText = ({ text, colors, }) => {
200
208
  const components = useMemo(() => ({
201
- p: ({ children }) => (_jsx("p", { style: { margin: '0 0 8px', fontSize: 14, lineHeight: 1.6, color: colors.text }, children: children })),
209
+ p: ({ children }) => (_jsx("p", { style: {
210
+ margin: '0 0 8px',
211
+ fontSize: 14,
212
+ lineHeight: 1.6,
213
+ color: colors.text,
214
+ }, children: children })),
202
215
  strong: ({ children }) => (_jsx("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
203
216
  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 })),
217
+ ul: ({ children }) => (_jsx("ul", { style: {
218
+ margin: '4px 0 8px',
219
+ paddingLeft: 20,
220
+ fontSize: 14,
221
+ color: colors.text,
222
+ }, children: children })),
223
+ ol: ({ children }) => (_jsx("ol", { style: {
224
+ margin: '4px 0 8px',
225
+ paddingLeft: 20,
226
+ fontSize: 14,
227
+ color: colors.text,
228
+ }, children: children })),
206
229
  li: ({ children }) => (_jsx("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
207
230
  code: ({ children, className }) => {
208
231
  const isBlock = className?.startsWith('language-');
209
232
  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 }) }));
233
+ return (_jsx("pre", { style: {
234
+ background: colors.codeBg,
235
+ padding: 10,
236
+ borderRadius: 4,
237
+ overflow: 'auto',
238
+ margin: '4px 0 8px',
239
+ fontSize: 12,
240
+ }, children: _jsx("code", { style: { color: colors.text }, children: children }) }));
211
241
  }
212
- return (_jsx("code", { style: { background: colors.codeBg, padding: '1px 4px', borderRadius: 3, fontSize: 13, color: colors.text }, children: children }));
242
+ return (_jsx("code", { style: {
243
+ background: colors.codeBg,
244
+ padding: '1px 4px',
245
+ borderRadius: 3,
246
+ fontSize: 13,
247
+ color: colors.text,
248
+ }, children: children }));
213
249
  },
214
250
  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 }),
251
+ h1: ({ children }) => (_jsx("h3", { style: {
252
+ margin: '8px 0 4px',
253
+ fontSize: 16,
254
+ fontWeight: 600,
255
+ color: colors.text,
256
+ }, children: children })),
257
+ h2: ({ children }) => (_jsx("h4", { style: {
258
+ margin: '8px 0 4px',
259
+ fontSize: 15,
260
+ fontWeight: 600,
261
+ color: colors.text,
262
+ }, children: children })),
263
+ h3: ({ children }) => (_jsx("h5", { style: {
264
+ margin: '8px 0 4px',
265
+ fontSize: 14,
266
+ fontWeight: 600,
267
+ color: colors.text,
268
+ }, children: children })),
218
269
  a: ({ href, children }) => (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", style: { color: colors.textMuted, textDecoration: 'underline' }, children: children })),
219
270
  }), [colors]);
220
271
  return _jsx(Markdown, { components: components, children: text });
@@ -235,11 +286,16 @@ const UserMessage = () => {
235
286
  borderRadius: 12,
236
287
  backgroundColor: colors.userBubble,
237
288
  }, children: _jsx(MessagePrimitive.Content, { components: {
238
- Text: ({ text }) => (_jsx("span", { style: { fontSize: 14, whiteSpace: 'pre-wrap', color: colors.text }, children: text })),
289
+ Text: ({ text }) => (_jsx("span", { style: {
290
+ fontSize: 14,
291
+ whiteSpace: 'pre-wrap',
292
+ color: colors.text,
293
+ }, children: text })),
239
294
  } }) })] }) }));
240
295
  };
241
296
  const AssistantMessage = () => {
242
297
  const colors = useContext(ColorsContext);
298
+ const toolComponents = useContext(ToolComponentsContext);
243
299
  return (_jsx("div", { style: {
244
300
  display: 'flex',
245
301
  justifyContent: 'flex-start',
@@ -249,8 +305,9 @@ const AssistantMessage = () => {
249
305
  borderRadius: 12,
250
306
  backgroundColor: colors.assistantBubble,
251
307
  }, children: [_jsx(MessagePrimitive.Content, { components: {
252
- Text: ({ text }) => (_jsx(MarkdownText, { text: text, colors: colors })),
308
+ Text: ({ text }) => _jsx(MarkdownText, { text: text, colors: colors }),
253
309
  tools: {
310
+ by_name: toolComponents,
254
311
  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
312
  },
256
313
  } }), _jsx(MessagePrimitive.If, { last: true, children: _jsx(ThreadPrimitive.If, { running: true, children: _jsx("div", { style: {
@@ -272,7 +329,9 @@ const PikkuComposer = ({ disabled, }) => {
272
329
  alignItems: 'flex-end',
273
330
  padding: '6px 12px',
274
331
  gap: 8,
275
- ...(disabled ? { opacity: 0.5, pointerEvents: 'none' } : {}),
332
+ ...(disabled
333
+ ? { opacity: 0.5, pointerEvents: 'none' }
334
+ : {}),
276
335
  }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
277
336
  flex: 1,
278
337
  border: 'none',
@@ -300,43 +359,48 @@ const PikkuComposer = ({ disabled, }) => {
300
359
  }, children: "\u25B6" })] }) }) }));
301
360
  };
302
361
  export function PikkuAgentChat(props) {
303
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768, ...runtimeOptions } = props;
362
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props;
304
363
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
305
364
  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: {
365
+ 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(AssistantRuntimeProvider, { runtime: runtime, children: _jsx("div", { style: {
366
+ height: '100%',
312
367
  display: 'flex',
313
368
  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: {
369
+ background: colors.bg,
370
+ }, children: _jsxs(ThreadPrimitive.Root, { style: {
371
+ display: 'flex',
372
+ flexDirection: 'column',
373
+ flex: 1,
374
+ minHeight: 0,
375
+ }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
376
+ flex: 1,
377
+ minHeight: 0,
378
+ overflowY: 'auto',
379
+ }, children: _jsxs("div", { style: {
380
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
381
+ margin: '0 auto',
382
+ padding: 16,
383
+ display: 'flex',
384
+ flexDirection: 'column',
385
+ gap: 16,
386
+ }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
387
+ display: 'flex',
388
+ alignItems: 'center',
389
+ justifyContent: 'center',
390
+ minHeight: 300,
391
+ color: colors.textMuted,
392
+ textAlign: 'center',
393
+ fontSize: 14,
394
+ }, children: emptyMessage ??
395
+ (props.threadId
396
+ ? 'Send a message to start the conversation.'
397
+ : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
398
+ UserMessage,
399
+ AssistantMessage,
400
+ } })] }) }), _jsx("div", { style: {
321
401
  maxWidth: maxWidth === 'none' ? undefined : maxWidth,
322
402
  margin: '0 auto',
323
- padding: 16,
324
- display: 'flex',
325
- 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 }) })] }) }) }) }) }) }));
403
+ width: '100%',
404
+ padding: '0 16px',
405
+ }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }) }));
342
406
  }
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.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,6 +5,7 @@ import {
5
5
  ThreadPrimitive,
6
6
  MessagePrimitive,
7
7
  ComposerPrimitive,
8
+ type ToolCallMessagePartComponent,
8
9
  } from '@assistant-ui/react'
9
10
  import {
10
11
  usePikkuAgentRuntime,
@@ -25,6 +26,17 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
25
26
  dark?: boolean
26
27
  /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
27
28
  maxWidth?: number | 'none'
29
+ /**
30
+ * Per-tool renderers. Map `toolName` → React component to replace the
31
+ * default expandable tool-call box for that tool. Enables generative-UI
32
+ * patterns: e.g. register a `renderWidget` tool on the agent and mount
33
+ * real UI (charts, diffs, cards) inline in the assistant bubble from the
34
+ * persisted tool-call args.
35
+ *
36
+ * Any tool without an entry here falls through to the default renderer
37
+ * (which still respects `hideToolCalls` and the approval-request UI).
38
+ */
39
+ toolComponents?: Record<string, ToolCallMessagePartComponent>
28
40
  }
29
41
 
30
42
  interface ChatColors {
@@ -92,6 +104,9 @@ const darkColors: ChatColors = {
92
104
 
93
105
  const ColorsContext = createContext<ChatColors>(lightColors)
94
106
  const HideToolCallsContext = createContext<boolean | string[] | undefined>(undefined)
107
+ const ToolComponentsContext = createContext<
108
+ Record<string, ToolCallMessagePartComponent> | undefined
109
+ >(undefined)
95
110
 
96
111
  function shouldHideToolCall(
97
112
  hideToolCalls: boolean | string[] | undefined,
@@ -470,6 +485,7 @@ const UserMessage: FunctionComponent = () => {
470
485
 
471
486
  const AssistantMessage: FunctionComponent = () => {
472
487
  const colors = useContext(ColorsContext)
488
+ const toolComponents = useContext(ToolComponentsContext)
473
489
  return (
474
490
  <div
475
491
  style={{
@@ -495,6 +511,7 @@ const AssistantMessage: FunctionComponent = () => {
495
511
  <MarkdownText text={text} colors={colors} />
496
512
  ),
497
513
  tools: {
514
+ by_name: toolComponents,
498
515
  Fallback: (props) => (
499
516
  <ToolCallDisplay
500
517
  toolCallId={props.toolCallId}
@@ -592,7 +609,7 @@ const PikkuComposer: FunctionComponent<{ disabled?: boolean }> = ({
592
609
  }
593
610
 
594
611
  export function PikkuAgentChat(props: PikkuAgentChatProps) {
595
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768, ...runtimeOptions } = props
612
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props
596
613
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } =
597
614
  usePikkuAgentRuntime(runtimeOptions)
598
615
 
@@ -602,6 +619,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
602
619
  <ColorsContext.Provider value={colors}>
603
620
  <PikkuApprovalContext.Provider value={{ pendingApprovals, handleApproval }}>
604
621
  <HideToolCallsContext.Provider value={hideToolCalls}>
622
+ <ToolComponentsContext.Provider value={toolComponents}>
605
623
  <AssistantRuntimeProvider runtime={runtime}>
606
624
  <div
607
625
  style={{
@@ -668,6 +686,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
668
686
  </ThreadPrimitive.Root>
669
687
  </div>
670
688
  </AssistantRuntimeProvider>
689
+ </ToolComponentsContext.Provider>
671
690
  </HideToolCallsContext.Provider>
672
691
  </PikkuApprovalContext.Provider>
673
692
  </ColorsContext.Provider>