@pikku/assistant-ui 0.12.2 → 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,47 @@
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
+
31
+ ## 0.12.3
32
+
33
+ ### Patch Changes
34
+
35
+ - f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
36
+
37
+ - Unified CredentialService with lazy loading per user via pikkuUserId
38
+ - wire.getCredential() for typed single credential lookup
39
+ - MissingCredentialError with structured payload for client-side connect flows
40
+ - Console UI: Global/Users credential tabs, per-user OAuth connect/revoke
41
+ - AI agent pre-flight check: detects missing OAuth credentials from addon metadata, shows "Connect your accounts" prompt before chat
42
+ - CLI codegen: generates credentialsMeta per addon package for runtime lookup
43
+ - Vercel AI runner: catches MissingCredentialError as runtime fallback
44
+
3
45
  ## 0.12.2
4
46
 
5
47
  ### Patch Changes
@@ -1,4 +1,4 @@
1
1
  export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalContext, usePikkuApproval, convertDbMessages, isDeniedResult, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
2
- export type { PikkuAgentRuntimeOptions, PendingApproval, PikkuApprovalContextValue, PikkuToolStatusType, PikkuToolStatus, } from './use-pikku-agent-runtime.js';
2
+ export type { PikkuAgentRuntimeOptions, PendingApproval, PikkuApprovalContextValue, PikkuToolStatusType, PikkuToolStatus, MissingCredentialPayload, } from './use-pikku-agent-runtime.js';
3
3
  export { PikkuAgentChat } from './pikku-agent-chat.js';
4
4
  export type { PikkuAgentChatProps } from './pikku-agent-chat.js';
@@ -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;
@@ -7,5 +8,18 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
7
8
  */
8
9
  hideToolCalls?: boolean | string[];
9
10
  dark?: boolean;
11
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
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>;
10
24
  }
11
25
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -34,6 +34,8 @@ const lightColors = {
34
34
  successColor: '#2e7d32',
35
35
  errorBg: '#ffebee',
36
36
  errorColor: '#c62828',
37
+ warningBg: '#fff3e0',
38
+ warningColor: '#e65100',
37
39
  };
38
40
  const darkColors = {
39
41
  bg: 'transparent',
@@ -52,9 +54,12 @@ const darkColors = {
52
54
  successColor: '#00e68a',
53
55
  errorBg: 'rgba(220, 38, 38, 0.1)',
54
56
  errorColor: '#f87171',
57
+ warningBg: 'rgba(245, 158, 11, 0.1)',
58
+ warningColor: '#fbbf24',
55
59
  };
56
60
  const ColorsContext = (0, react_1.createContext)(lightColors);
57
61
  const HideToolCallsContext = (0, react_1.createContext)(undefined);
62
+ const ToolComponentsContext = (0, react_1.createContext)(undefined);
58
63
  function shouldHideToolCall(hideToolCalls, toolName) {
59
64
  if (!hideToolCalls)
60
65
  return false;
@@ -145,7 +150,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
145
150
  padding: '2px 6px',
146
151
  borderRadius: 3,
147
152
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
148
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
153
+ color: responded === 'approved'
154
+ ? colors.successColor
155
+ : colors.errorColor,
149
156
  }, children: responded })] }));
150
157
  }
151
158
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
@@ -170,7 +177,13 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
170
177
  borderRadius: 3,
171
178
  background: colors.errorBg,
172
179
  color: colors.errorColor,
173
- }, children: "error" })), status.type === 'denied' && ((0, jsx_runtime_1.jsx)("span", { style: {
180
+ }, children: "error" })), status.type === 'missing-credential' && ((0, jsx_runtime_1.jsx)("span", { style: {
181
+ fontSize: 11,
182
+ padding: '1px 5px',
183
+ borderRadius: 3,
184
+ background: colors.warningBg,
185
+ color: colors.warningColor,
186
+ }, children: "credential required" })), status.type === 'denied' && ((0, jsx_runtime_1.jsx)("span", { style: {
174
187
  fontSize: 11,
175
188
  padding: '1px 5px',
176
189
  borderRadius: 3,
@@ -189,7 +202,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
189
202
  borderRadius: 4,
190
203
  overflow: 'auto',
191
204
  color: colors.text,
192
- }, 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: {
193
211
  fontSize: 11,
194
212
  background: colors.codeBg,
195
213
  padding: 8,
@@ -200,25 +218,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
200
218
  ? result
201
219
  : JSON.stringify(result, null, 2) })] }))] }))] }));
202
220
  };
203
- const MarkdownText = ({ text, colors }) => {
221
+ const MarkdownText = ({ text, colors, }) => {
204
222
  const components = (0, react_1.useMemo)(() => ({
205
- 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 })),
206
229
  strong: ({ children }) => ((0, jsx_runtime_1.jsx)("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
207
230
  em: ({ children }) => ((0, jsx_runtime_1.jsx)("em", { style: { color: colors.text }, children: children })),
208
- ul: ({ children }) => ((0, jsx_runtime_1.jsx)("ul", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
209
- 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 })),
210
243
  li: ({ children }) => ((0, jsx_runtime_1.jsx)("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
211
244
  code: ({ children, className }) => {
212
245
  const isBlock = className === null || className === void 0 ? void 0 : className.startsWith('language-');
213
246
  if (isBlock) {
214
- 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 }) }));
215
255
  }
216
- 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 }));
217
263
  },
218
264
  pre: ({ children }) => (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children }),
219
- h1: ({ children }) => (0, jsx_runtime_1.jsx)("h3", { style: { margin: '8px 0 4px', fontSize: 16, fontWeight: 600, color: colors.text }, children: children }),
220
- h2: ({ children }) => (0, jsx_runtime_1.jsx)("h4", { style: { margin: '8px 0 4px', fontSize: 15, fontWeight: 600, color: colors.text }, children: children }),
221
- 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 })),
222
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 })),
223
284
  }), [colors]);
224
285
  return (0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: components, children: text });
@@ -239,11 +300,16 @@ const UserMessage = () => {
239
300
  borderRadius: 12,
240
301
  backgroundColor: colors.userBubble,
241
302
  }, children: (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: {
242
- 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 })),
243
308
  } }) })] }) }));
244
309
  };
245
310
  const AssistantMessage = () => {
246
311
  const colors = (0, react_1.useContext)(ColorsContext);
312
+ const toolComponents = (0, react_1.useContext)(ToolComponentsContext);
247
313
  return ((0, jsx_runtime_1.jsx)("div", { style: {
248
314
  display: 'flex',
249
315
  justifyContent: 'flex-start',
@@ -253,8 +319,9 @@ const AssistantMessage = () => {
253
319
  borderRadius: 12,
254
320
  backgroundColor: colors.assistantBubble,
255
321
  }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Content, { components: {
256
- 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 }),
257
323
  tools: {
324
+ by_name: toolComponents,
258
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 })),
259
326
  },
260
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: {
@@ -268,7 +335,9 @@ const AssistantMessage = () => {
268
335
  };
269
336
  const PikkuComposer = ({ disabled, }) => {
270
337
  const colors = (0, react_1.useContext)(ColorsContext);
271
- 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: {
272
341
  flex: 1,
273
342
  border: 'none',
274
343
  outline: 'none',
@@ -295,42 +364,47 @@ const PikkuComposer = ({ disabled, }) => {
295
364
  }, children: "\u25B6" })] }) }) }));
296
365
  };
297
366
  function PikkuAgentChat(props) {
298
- const { emptyMessage, hideToolCalls, dark } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark"]);
367
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth", "toolComponents"]);
299
368
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = (0, use_pikku_agent_runtime_js_1.usePikkuAgentRuntime)(runtimeOptions);
300
369
  const colors = dark ? darkColors : lightColors;
301
- 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: {
302
- height: '100%',
303
- display: 'flex',
304
- flexDirection: 'column',
305
- background: colors.bg,
306
- }, 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%',
307
372
  display: 'flex',
308
373
  flexDirection: 'column',
309
- flex: 1,
310
- minHeight: 0,
311
- }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Viewport, { style: {
312
- flex: 1,
313
- minHeight: 0,
314
- overflowY: 'auto',
315
- }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
316
- maxWidth: 768,
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: {
405
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
317
406
  margin: '0 auto',
318
- padding: 16,
319
- display: 'flex',
320
- flexDirection: 'column',
321
- gap: 16,
322
- }, children: [(0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Empty, { children: (0, jsx_runtime_1.jsx)("div", { style: {
323
- display: 'flex',
324
- alignItems: 'center',
325
- justifyContent: 'center',
326
- minHeight: 300,
327
- color: colors.textMuted,
328
- textAlign: 'center',
329
- fontSize: 14,
330
- }, children: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : (props.threadId
331
- ? 'Send a message to start the conversation.'
332
- : 'Start a new conversation.') }) }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
333
- UserMessage,
334
- AssistantMessage,
335
- } })] }) }), (0, jsx_runtime_1.jsx)("div", { style: { maxWidth: 768, 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 }) })] }) }) }) }) }) }) }));
336
410
  }
@@ -17,6 +17,10 @@ export interface PendingApproval {
17
17
  args: unknown;
18
18
  reason?: string;
19
19
  runId: string;
20
+ type?: 'approval-request' | 'credential-request';
21
+ credentialName?: string;
22
+ credentialType?: 'oauth2' | 'apikey';
23
+ connectUrl?: string;
20
24
  }
21
25
  export interface PikkuApprovalContextValue {
22
26
  pendingApprovals: PendingApproval[];
@@ -25,9 +29,18 @@ export interface PikkuApprovalContextValue {
25
29
  export declare const PikkuApprovalContext: import("react").Context<PikkuApprovalContextValue>;
26
30
  export declare const usePikkuApproval: () => PikkuApprovalContextValue;
27
31
  export declare function isDeniedResult(result: unknown): boolean;
28
- export type PikkuToolStatusType = 'running' | 'requires-action' | 'completed' | 'denied' | 'error';
32
+ export type PikkuToolStatusType = 'running' | 'requires-action' | 'completed' | 'denied' | 'error' | 'missing-credential';
33
+ export type MissingCredentialPayload = {
34
+ error: 'missing_credential';
35
+ credentialName: string;
36
+ credentialType: 'oauth2' | 'apikey';
37
+ connectUrl?: string;
38
+ };
29
39
  export type PikkuToolStatus = {
30
- type: PikkuToolStatusType;
40
+ type: Exclude<PikkuToolStatusType, 'missing-credential'>;
41
+ } | {
42
+ type: 'missing-credential';
43
+ payload: MissingCredentialPayload;
31
44
  };
32
45
  export declare function resolvePikkuToolStatus(status: {
33
46
  type: string;
@@ -129,6 +129,19 @@ function processStream(reader, text, toolCalls, yieldContent, onFinish) {
129
129
  args: event.args,
130
130
  reason: event.reason,
131
131
  runId: event.runId,
132
+ type: 'approval-request',
133
+ });
134
+ break;
135
+ case 'credential-request':
136
+ pendingApprovals.push({
137
+ toolCallId: event.toolCallId,
138
+ toolName: event.toolName,
139
+ args: event.args,
140
+ runId: event.runId,
141
+ type: 'credential-request',
142
+ credentialName: event.credentialName,
143
+ credentialType: event.credentialType,
144
+ connectUrl: event.connectUrl,
132
145
  });
133
146
  break;
134
147
  case 'error':
@@ -162,6 +175,22 @@ function isDeniedResult(result) {
162
175
  return false;
163
176
  }
164
177
  }
178
+ function isMissingCredentialResult(result) {
179
+ if (typeof result === 'object' &&
180
+ result &&
181
+ result.error === 'missing_credential') {
182
+ return result;
183
+ }
184
+ if (typeof result === 'string') {
185
+ try {
186
+ const parsed = JSON.parse(result);
187
+ if ((parsed === null || parsed === void 0 ? void 0 : parsed.error) === 'missing_credential')
188
+ return parsed;
189
+ }
190
+ catch (_a) { }
191
+ }
192
+ return null;
193
+ }
165
194
  function resolvePikkuToolStatus(status, result) {
166
195
  if (status.type === 'running')
167
196
  return { type: 'running' };
@@ -169,6 +198,9 @@ function resolvePikkuToolStatus(status, result) {
169
198
  return { type: 'requires-action' };
170
199
  if (isDeniedResult(result))
171
200
  return { type: 'denied' };
201
+ const missingCred = isMissingCredentialResult(result);
202
+ if (missingCred)
203
+ return { type: 'missing-credential', payload: missingCred };
172
204
  if (typeof result === 'string' && result.startsWith('Error:'))
173
205
  return { type: 'error' };
174
206
  return { type: 'completed' };
@@ -315,8 +347,11 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
315
347
  catch (e) {
316
348
  const msg = (e === null || e === void 0 ? void 0 : e.message) || 'Unknown error';
317
349
  let errorText = 'Failed to connect to the agent.';
318
- if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || msg.includes('CORS')) {
319
- errorText = 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.';
350
+ if (msg.includes('Failed to fetch') ||
351
+ msg.includes('NetworkError') ||
352
+ msg.includes('CORS')) {
353
+ errorText =
354
+ 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.';
320
355
  }
321
356
  else if (msg.includes('abort')) {
322
357
  return yield __await(void 0);
@@ -330,10 +365,12 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
330
365
  errorText = 'Authentication error.';
331
366
  }
332
367
  else if (response.status === 404) {
333
- errorText = 'Agent not found — the agent may not be configured for this project.';
368
+ errorText =
369
+ 'Agent not found — the agent may not be configured for this project.';
334
370
  }
335
371
  else if (response.status === 502 || response.status === 503) {
336
- errorText = 'The agent server is currently unavailable. Try again in a moment.';
372
+ errorText =
373
+ 'The agent server is currently unavailable. Try again in a moment.';
337
374
  }
338
375
  else if (response.status === 429) {
339
376
  errorText = 'Rate limited — too many requests. Please wait a moment.';
@@ -1,4 +1,4 @@
1
1
  export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalContext, usePikkuApproval, convertDbMessages, isDeniedResult, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
2
- export type { PikkuAgentRuntimeOptions, PendingApproval, PikkuApprovalContextValue, PikkuToolStatusType, PikkuToolStatus, } from './use-pikku-agent-runtime.js';
2
+ export type { PikkuAgentRuntimeOptions, PendingApproval, PikkuApprovalContextValue, PikkuToolStatusType, PikkuToolStatus, MissingCredentialPayload, } from './use-pikku-agent-runtime.js';
3
3
  export { PikkuAgentChat } from './pikku-agent-chat.js';
4
4
  export type { PikkuAgentChatProps } from './pikku-agent-chat.js';
@@ -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;
@@ -7,5 +8,18 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
7
8
  */
8
9
  hideToolCalls?: boolean | string[];
9
10
  dark?: boolean;
11
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
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>;
10
24
  }
11
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';
@@ -20,6 +20,8 @@ const lightColors = {
20
20
  successColor: '#2e7d32',
21
21
  errorBg: '#ffebee',
22
22
  errorColor: '#c62828',
23
+ warningBg: '#fff3e0',
24
+ warningColor: '#e65100',
23
25
  };
24
26
  const darkColors = {
25
27
  bg: 'transparent',
@@ -38,9 +40,12 @@ const darkColors = {
38
40
  successColor: '#00e68a',
39
41
  errorBg: 'rgba(220, 38, 38, 0.1)',
40
42
  errorColor: '#f87171',
43
+ warningBg: 'rgba(245, 158, 11, 0.1)',
44
+ warningColor: '#fbbf24',
41
45
  };
42
46
  const ColorsContext = createContext(lightColors);
43
47
  const HideToolCallsContext = createContext(undefined);
48
+ const ToolComponentsContext = createContext(undefined);
44
49
  function shouldHideToolCall(hideToolCalls, toolName) {
45
50
  if (!hideToolCalls)
46
51
  return false;
@@ -131,7 +136,9 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
131
136
  padding: '2px 6px',
132
137
  borderRadius: 3,
133
138
  background: responded === 'approved' ? colors.successBg : colors.errorBg,
134
- color: responded === 'approved' ? colors.successColor : colors.errorColor,
139
+ color: responded === 'approved'
140
+ ? colors.successColor
141
+ : colors.errorColor,
135
142
  }, children: responded })] }));
136
143
  }
137
144
  return (_jsxs("div", { style: {
@@ -156,7 +163,13 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
156
163
  borderRadius: 3,
157
164
  background: colors.errorBg,
158
165
  color: colors.errorColor,
159
- }, children: "error" })), status.type === 'denied' && (_jsx("span", { style: {
166
+ }, children: "error" })), status.type === 'missing-credential' && (_jsx("span", { style: {
167
+ fontSize: 11,
168
+ padding: '1px 5px',
169
+ borderRadius: 3,
170
+ background: colors.warningBg,
171
+ color: colors.warningColor,
172
+ }, children: "credential required" })), status.type === 'denied' && (_jsx("span", { style: {
160
173
  fontSize: 11,
161
174
  padding: '1px 5px',
162
175
  borderRadius: 3,
@@ -175,7 +188,12 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
175
188
  borderRadius: 4,
176
189
  overflow: 'auto',
177
190
  color: colors.text,
178
- }, 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: {
179
197
  fontSize: 11,
180
198
  background: colors.codeBg,
181
199
  padding: 8,
@@ -186,25 +204,68 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
186
204
  ? result
187
205
  : JSON.stringify(result, null, 2) })] }))] }))] }));
188
206
  };
189
- const MarkdownText = ({ text, colors }) => {
207
+ const MarkdownText = ({ text, colors, }) => {
190
208
  const components = useMemo(() => ({
191
- 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 })),
192
215
  strong: ({ children }) => (_jsx("strong", { style: { fontWeight: 600, color: colors.text }, children: children })),
193
216
  em: ({ children }) => (_jsx("em", { style: { color: colors.text }, children: children })),
194
- ul: ({ children }) => (_jsx("ul", { style: { margin: '4px 0 8px', paddingLeft: 20, fontSize: 14, color: colors.text }, children: children })),
195
- 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 })),
196
229
  li: ({ children }) => (_jsx("li", { style: { marginBottom: 2, lineHeight: 1.6 }, children: children })),
197
230
  code: ({ children, className }) => {
198
231
  const isBlock = className?.startsWith('language-');
199
232
  if (isBlock) {
200
- 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 }) }));
201
241
  }
202
- 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 }));
203
249
  },
204
250
  pre: ({ children }) => _jsx(_Fragment, { children: children }),
205
- h1: ({ children }) => _jsx("h3", { style: { margin: '8px 0 4px', fontSize: 16, fontWeight: 600, color: colors.text }, children: children }),
206
- h2: ({ children }) => _jsx("h4", { style: { margin: '8px 0 4px', fontSize: 15, fontWeight: 600, color: colors.text }, children: children }),
207
- 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 })),
208
269
  a: ({ href, children }) => (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", style: { color: colors.textMuted, textDecoration: 'underline' }, children: children })),
209
270
  }), [colors]);
210
271
  return _jsx(Markdown, { components: components, children: text });
@@ -225,11 +286,16 @@ const UserMessage = () => {
225
286
  borderRadius: 12,
226
287
  backgroundColor: colors.userBubble,
227
288
  }, children: _jsx(MessagePrimitive.Content, { components: {
228
- 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 })),
229
294
  } }) })] }) }));
230
295
  };
231
296
  const AssistantMessage = () => {
232
297
  const colors = useContext(ColorsContext);
298
+ const toolComponents = useContext(ToolComponentsContext);
233
299
  return (_jsx("div", { style: {
234
300
  display: 'flex',
235
301
  justifyContent: 'flex-start',
@@ -239,8 +305,9 @@ const AssistantMessage = () => {
239
305
  borderRadius: 12,
240
306
  backgroundColor: colors.assistantBubble,
241
307
  }, children: [_jsx(MessagePrimitive.Content, { components: {
242
- Text: ({ text }) => (_jsx(MarkdownText, { text: text, colors: colors })),
308
+ Text: ({ text }) => _jsx(MarkdownText, { text: text, colors: colors }),
243
309
  tools: {
310
+ by_name: toolComponents,
244
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 })),
245
312
  },
246
313
  } }), _jsx(MessagePrimitive.If, { last: true, children: _jsx(ThreadPrimitive.If, { running: true, children: _jsx("div", { style: {
@@ -262,7 +329,9 @@ const PikkuComposer = ({ disabled, }) => {
262
329
  alignItems: 'flex-end',
263
330
  padding: '6px 12px',
264
331
  gap: 8,
265
- ...(disabled ? { opacity: 0.5, pointerEvents: 'none' } : {}),
332
+ ...(disabled
333
+ ? { opacity: 0.5, pointerEvents: 'none' }
334
+ : {}),
266
335
  }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
267
336
  flex: 1,
268
337
  border: 'none',
@@ -290,43 +359,48 @@ const PikkuComposer = ({ disabled, }) => {
290
359
  }, children: "\u25B6" })] }) }) }));
291
360
  };
292
361
  export function PikkuAgentChat(props) {
293
- const { emptyMessage, hideToolCalls, dark, ...runtimeOptions } = props;
362
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props;
294
363
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
295
364
  const colors = dark ? darkColors : lightColors;
296
- 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: {
297
- height: '100%',
298
- display: 'flex',
299
- flexDirection: 'column',
300
- background: colors.bg,
301
- }, 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%',
302
367
  display: 'flex',
303
368
  flexDirection: 'column',
304
- flex: 1,
305
- minHeight: 0,
306
- }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
307
- flex: 1,
308
- minHeight: 0,
309
- overflowY: 'auto',
310
- }, children: _jsxs("div", { style: {
311
- maxWidth: 768,
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: {
401
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
312
402
  margin: '0 auto',
313
- padding: 16,
314
- display: 'flex',
315
- flexDirection: 'column',
316
- gap: 16,
317
- }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
318
- display: 'flex',
319
- alignItems: 'center',
320
- justifyContent: 'center',
321
- minHeight: 300,
322
- color: colors.textMuted,
323
- textAlign: 'center',
324
- fontSize: 14,
325
- }, children: emptyMessage ??
326
- (props.threadId
327
- ? 'Send a message to start the conversation.'
328
- : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
329
- UserMessage,
330
- AssistantMessage,
331
- } })] }) }), _jsx("div", { style: { maxWidth: 768, 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 }) })] }) }) }) }) }) }) }));
332
406
  }
@@ -17,6 +17,10 @@ export interface PendingApproval {
17
17
  args: unknown;
18
18
  reason?: string;
19
19
  runId: string;
20
+ type?: 'approval-request' | 'credential-request';
21
+ credentialName?: string;
22
+ credentialType?: 'oauth2' | 'apikey';
23
+ connectUrl?: string;
20
24
  }
21
25
  export interface PikkuApprovalContextValue {
22
26
  pendingApprovals: PendingApproval[];
@@ -25,9 +29,18 @@ export interface PikkuApprovalContextValue {
25
29
  export declare const PikkuApprovalContext: import("react").Context<PikkuApprovalContextValue>;
26
30
  export declare const usePikkuApproval: () => PikkuApprovalContextValue;
27
31
  export declare function isDeniedResult(result: unknown): boolean;
28
- export type PikkuToolStatusType = 'running' | 'requires-action' | 'completed' | 'denied' | 'error';
32
+ export type PikkuToolStatusType = 'running' | 'requires-action' | 'completed' | 'denied' | 'error' | 'missing-credential';
33
+ export type MissingCredentialPayload = {
34
+ error: 'missing_credential';
35
+ credentialName: string;
36
+ credentialType: 'oauth2' | 'apikey';
37
+ connectUrl?: string;
38
+ };
29
39
  export type PikkuToolStatus = {
30
- type: PikkuToolStatusType;
40
+ type: Exclude<PikkuToolStatusType, 'missing-credential'>;
41
+ } | {
42
+ type: 'missing-credential';
43
+ payload: MissingCredentialPayload;
31
44
  };
32
45
  export declare function resolvePikkuToolStatus(status: {
33
46
  type: string;
@@ -84,6 +84,19 @@ async function processStream(reader, text, toolCalls, yieldContent, onFinish) {
84
84
  args: event.args,
85
85
  reason: event.reason,
86
86
  runId: event.runId,
87
+ type: 'approval-request',
88
+ });
89
+ break;
90
+ case 'credential-request':
91
+ pendingApprovals.push({
92
+ toolCallId: event.toolCallId,
93
+ toolName: event.toolName,
94
+ args: event.args,
95
+ runId: event.runId,
96
+ type: 'credential-request',
97
+ credentialName: event.credentialName,
98
+ credentialType: event.credentialType,
99
+ connectUrl: event.connectUrl,
87
100
  });
88
101
  break;
89
102
  case 'error':
@@ -108,6 +121,22 @@ export function isDeniedResult(result) {
108
121
  return false;
109
122
  }
110
123
  }
124
+ function isMissingCredentialResult(result) {
125
+ if (typeof result === 'object' &&
126
+ result &&
127
+ result.error === 'missing_credential') {
128
+ return result;
129
+ }
130
+ if (typeof result === 'string') {
131
+ try {
132
+ const parsed = JSON.parse(result);
133
+ if (parsed?.error === 'missing_credential')
134
+ return parsed;
135
+ }
136
+ catch { }
137
+ }
138
+ return null;
139
+ }
111
140
  export function resolvePikkuToolStatus(status, result) {
112
141
  if (status.type === 'running')
113
142
  return { type: 'running' };
@@ -115,6 +144,9 @@ export function resolvePikkuToolStatus(status, result) {
115
144
  return { type: 'requires-action' };
116
145
  if (isDeniedResult(result))
117
146
  return { type: 'denied' };
147
+ const missingCred = isMissingCredentialResult(result);
148
+ if (missingCred)
149
+ return { type: 'missing-credential', payload: missingCred };
118
150
  if (typeof result === 'string' && result.startsWith('Error:'))
119
151
  return { type: 'error' };
120
152
  return { type: 'completed' };
@@ -263,8 +295,11 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
263
295
  catch (e) {
264
296
  const msg = e?.message || 'Unknown error';
265
297
  let errorText = 'Failed to connect to the agent.';
266
- if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || msg.includes('CORS')) {
267
- errorText = 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.';
298
+ if (msg.includes('Failed to fetch') ||
299
+ msg.includes('NetworkError') ||
300
+ msg.includes('CORS')) {
301
+ errorText =
302
+ 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.';
268
303
  }
269
304
  else if (msg.includes('abort')) {
270
305
  return;
@@ -278,10 +313,12 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
278
313
  errorText = 'Authentication error.';
279
314
  }
280
315
  else if (response.status === 404) {
281
- errorText = 'Agent not found — the agent may not be configured for this project.';
316
+ errorText =
317
+ 'Agent not found — the agent may not be configured for this project.';
282
318
  }
283
319
  else if (response.status === 502 || response.status === 503) {
284
- errorText = 'The agent server is currently unavailable. Try again in a moment.';
320
+ errorText =
321
+ 'The agent server is currently unavailable. Try again in a moment.';
285
322
  }
286
323
  else if (response.status === 429) {
287
324
  errorText = 'Rate limited — too many requests. Please wait a moment.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  PikkuApprovalContextValue,
14
14
  PikkuToolStatusType,
15
15
  PikkuToolStatus,
16
+ MissingCredentialPayload,
16
17
  } from './use-pikku-agent-runtime.js'
17
18
  export { PikkuAgentChat } from './pikku-agent-chat.js'
18
19
  export type { PikkuAgentChatProps } from './pikku-agent-chat.js'
@@ -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,
@@ -23,6 +24,19 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
23
24
  */
24
25
  hideToolCalls?: boolean | string[]
25
26
  dark?: boolean
27
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
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>
26
40
  }
27
41
 
28
42
  interface ChatColors {
@@ -42,6 +56,8 @@ interface ChatColors {
42
56
  successColor: string
43
57
  errorBg: string
44
58
  errorColor: string
59
+ warningBg: string
60
+ warningColor: string
45
61
  }
46
62
 
47
63
  const lightColors: ChatColors = {
@@ -61,6 +77,8 @@ const lightColors: ChatColors = {
61
77
  successColor: '#2e7d32',
62
78
  errorBg: '#ffebee',
63
79
  errorColor: '#c62828',
80
+ warningBg: '#fff3e0',
81
+ warningColor: '#e65100',
64
82
  }
65
83
 
66
84
  const darkColors: ChatColors = {
@@ -80,10 +98,15 @@ const darkColors: ChatColors = {
80
98
  successColor: '#00e68a',
81
99
  errorBg: 'rgba(220, 38, 38, 0.1)',
82
100
  errorColor: '#f87171',
101
+ warningBg: 'rgba(245, 158, 11, 0.1)',
102
+ warningColor: '#fbbf24',
83
103
  }
84
104
 
85
105
  const ColorsContext = createContext<ChatColors>(lightColors)
86
106
  const HideToolCallsContext = createContext<boolean | string[] | undefined>(undefined)
107
+ const ToolComponentsContext = createContext<
108
+ Record<string, ToolCallMessagePartComponent> | undefined
109
+ >(undefined)
87
110
 
88
111
  function shouldHideToolCall(
89
112
  hideToolCalls: boolean | string[] | undefined,
@@ -284,6 +307,19 @@ const ToolCallDisplay: FunctionComponent<{
284
307
  error
285
308
  </span>
286
309
  )}
310
+ {status.type === 'missing-credential' && (
311
+ <span
312
+ style={{
313
+ fontSize: 11,
314
+ padding: '1px 5px',
315
+ borderRadius: 3,
316
+ background: colors.warningBg,
317
+ color: colors.warningColor,
318
+ }}
319
+ >
320
+ credential required
321
+ </span>
322
+ )}
287
323
  {status.type === 'denied' && (
288
324
  <span
289
325
  style={{
@@ -449,6 +485,7 @@ const UserMessage: FunctionComponent = () => {
449
485
 
450
486
  const AssistantMessage: FunctionComponent = () => {
451
487
  const colors = useContext(ColorsContext)
488
+ const toolComponents = useContext(ToolComponentsContext)
452
489
  return (
453
490
  <div
454
491
  style={{
@@ -474,6 +511,7 @@ const AssistantMessage: FunctionComponent = () => {
474
511
  <MarkdownText text={text} colors={colors} />
475
512
  ),
476
513
  tools: {
514
+ by_name: toolComponents,
477
515
  Fallback: (props) => (
478
516
  <ToolCallDisplay
479
517
  toolCallId={props.toolCallId}
@@ -571,7 +609,7 @@ const PikkuComposer: FunctionComponent<{ disabled?: boolean }> = ({
571
609
  }
572
610
 
573
611
  export function PikkuAgentChat(props: PikkuAgentChatProps) {
574
- const { emptyMessage, hideToolCalls, dark, ...runtimeOptions } = props
612
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props
575
613
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } =
576
614
  usePikkuAgentRuntime(runtimeOptions)
577
615
 
@@ -581,6 +619,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
581
619
  <ColorsContext.Provider value={colors}>
582
620
  <PikkuApprovalContext.Provider value={{ pendingApprovals, handleApproval }}>
583
621
  <HideToolCallsContext.Provider value={hideToolCalls}>
622
+ <ToolComponentsContext.Provider value={toolComponents}>
584
623
  <AssistantRuntimeProvider runtime={runtime}>
585
624
  <div
586
625
  style={{
@@ -607,7 +646,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
607
646
  >
608
647
  <div
609
648
  style={{
610
- maxWidth: 768,
649
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
611
650
  margin: '0 auto',
612
651
  padding: 16,
613
652
  display: 'flex',
@@ -641,12 +680,13 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
641
680
  />
642
681
  </div>
643
682
  </ThreadPrimitive.Viewport>
644
- <div style={{ maxWidth: 768, margin: '0 auto', width: '100%', padding: '0 16px' }}>
683
+ <div style={{ maxWidth: maxWidth === 'none' ? undefined : maxWidth, margin: '0 auto', width: '100%', padding: '0 16px' }}>
645
684
  <PikkuComposer disabled={isAwaitingApproval} />
646
685
  </div>
647
686
  </ThreadPrimitive.Root>
648
687
  </div>
649
688
  </AssistantRuntimeProvider>
689
+ </ToolComponentsContext.Provider>
650
690
  </HideToolCallsContext.Provider>
651
691
  </PikkuApprovalContext.Provider>
652
692
  </ColorsContext.Provider>
@@ -31,6 +31,10 @@ export interface PendingApproval {
31
31
  args: unknown
32
32
  reason?: string
33
33
  runId: string
34
+ type?: 'approval-request' | 'credential-request'
35
+ credentialName?: string
36
+ credentialType?: 'oauth2' | 'apikey'
37
+ connectUrl?: string
34
38
  }
35
39
 
36
40
  export interface PikkuApprovalContextValue {
@@ -143,6 +147,19 @@ async function processStream(
143
147
  args: event.args,
144
148
  reason: event.reason,
145
149
  runId: event.runId,
150
+ type: 'approval-request',
151
+ })
152
+ break
153
+ case 'credential-request':
154
+ pendingApprovals.push({
155
+ toolCallId: event.toolCallId,
156
+ toolName: event.toolName,
157
+ args: event.args,
158
+ runId: event.runId,
159
+ type: 'credential-request',
160
+ credentialName: event.credentialName,
161
+ credentialType: event.credentialType,
162
+ connectUrl: event.connectUrl,
146
163
  })
147
164
  break
148
165
  case 'error':
@@ -174,8 +191,37 @@ export type PikkuToolStatusType =
174
191
  | 'completed'
175
192
  | 'denied'
176
193
  | 'error'
194
+ | 'missing-credential'
177
195
 
178
- export type PikkuToolStatus = { type: PikkuToolStatusType }
196
+ export type MissingCredentialPayload = {
197
+ error: 'missing_credential'
198
+ credentialName: string
199
+ credentialType: 'oauth2' | 'apikey'
200
+ connectUrl?: string
201
+ }
202
+
203
+ export type PikkuToolStatus =
204
+ | { type: Exclude<PikkuToolStatusType, 'missing-credential'> }
205
+ | { type: 'missing-credential'; payload: MissingCredentialPayload }
206
+
207
+ function isMissingCredentialResult(
208
+ result: unknown
209
+ ): MissingCredentialPayload | null {
210
+ if (
211
+ typeof result === 'object' &&
212
+ result &&
213
+ (result as any).error === 'missing_credential'
214
+ ) {
215
+ return result as MissingCredentialPayload
216
+ }
217
+ if (typeof result === 'string') {
218
+ try {
219
+ const parsed = JSON.parse(result)
220
+ if (parsed?.error === 'missing_credential') return parsed
221
+ } catch {}
222
+ }
223
+ return null
224
+ }
179
225
 
180
226
  export function resolvePikkuToolStatus(
181
227
  status: { type: string },
@@ -184,6 +230,8 @@ export function resolvePikkuToolStatus(
184
230
  if (status.type === 'running') return { type: 'running' }
185
231
  if (status.type === 'requires-action') return { type: 'requires-action' }
186
232
  if (isDeniedResult(result)) return { type: 'denied' }
233
+ const missingCred = isMissingCredentialResult(result)
234
+ if (missingCred) return { type: 'missing-credential', payload: missingCred }
187
235
  if (typeof result === 'string' && result.startsWith('Error:'))
188
236
  return { type: 'error' }
189
237
  return { type: 'completed' }
@@ -370,8 +418,13 @@ function createPikkuStreamingAdapter(
370
418
  } catch (e: any) {
371
419
  const msg = e?.message || 'Unknown error'
372
420
  let errorText = 'Failed to connect to the agent.'
373
- if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || msg.includes('CORS')) {
374
- errorText = 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.'
421
+ if (
422
+ msg.includes('Failed to fetch') ||
423
+ msg.includes('NetworkError') ||
424
+ msg.includes('CORS')
425
+ ) {
426
+ errorText =
427
+ 'Unable to reach the agent server. The deployment may be down or the URL may be incorrect.'
375
428
  } else if (msg.includes('abort')) {
376
429
  return
377
430
  }
@@ -384,9 +437,11 @@ function createPikkuStreamingAdapter(
384
437
  if (response.status === 401 || response.status === 403) {
385
438
  errorText = 'Authentication error.'
386
439
  } else if (response.status === 404) {
387
- errorText = 'Agent not found — the agent may not be configured for this project.'
440
+ errorText =
441
+ 'Agent not found — the agent may not be configured for this project.'
388
442
  } else if (response.status === 502 || response.status === 503) {
389
- errorText = 'The agent server is currently unavailable. Try again in a moment.'
443
+ errorText =
444
+ 'The agent server is currently unavailable. Try again in a moment.'
390
445
  } else if (response.status === 429) {
391
446
  errorText = 'Rate limited — too many requests. Please wait a moment.'
392
447
  }