@pikku/assistant-ui 0.12.1 → 0.12.3

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,25 @@
1
1
  # @pikku/assistant-ui
2
2
 
3
+ ## 0.12.3
4
+
5
+ ### Patch Changes
6
+
7
+ - f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
8
+
9
+ - Unified CredentialService with lazy loading per user via pikkuUserId
10
+ - wire.getCredential() for typed single credential lookup
11
+ - MissingCredentialError with structured payload for client-side connect flows
12
+ - Console UI: Global/Users credential tabs, per-user OAuth connect/revoke
13
+ - AI agent pre-flight check: detects missing OAuth credentials from addon metadata, shows "Connect your accounts" prompt before chat
14
+ - CLI codegen: generates credentialsMeta per addon package for runtime lookup
15
+ - Vercel AI runner: catches MissingCredentialError as runtime fallback
16
+
17
+ ## 0.12.2
18
+
19
+ ### Patch Changes
20
+
21
+ - cc4a8e0: Show friendly error messages in agent chat instead of silently failing with a loading spinner
22
+
3
23
  ## 0.12.1
4
24
 
5
25
  ### 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';
@@ -7,5 +7,7 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
7
7
  */
8
8
  hideToolCalls?: boolean | string[];
9
9
  dark?: boolean;
10
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
+ maxWidth?: number | 'none';
10
12
  }
11
13
  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,6 +54,8 @@ 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);
@@ -170,7 +174,13 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
170
174
  borderRadius: 3,
171
175
  background: colors.errorBg,
172
176
  color: colors.errorColor,
173
- }, children: "error" })), status.type === 'denied' && ((0, jsx_runtime_1.jsx)("span", { style: {
177
+ }, children: "error" })), status.type === 'missing-credential' && ((0, jsx_runtime_1.jsx)("span", { style: {
178
+ fontSize: 11,
179
+ padding: '1px 5px',
180
+ borderRadius: 3,
181
+ background: colors.warningBg,
182
+ color: colors.warningColor,
183
+ }, children: "credential required" })), status.type === 'denied' && ((0, jsx_runtime_1.jsx)("span", { style: {
174
184
  fontSize: 11,
175
185
  padding: '1px 5px',
176
186
  borderRadius: 3,
@@ -295,7 +305,7 @@ const PikkuComposer = ({ disabled, }) => {
295
305
  }, children: "\u25B6" })] }) }) }));
296
306
  };
297
307
  function PikkuAgentChat(props) {
298
- const { emptyMessage, hideToolCalls, dark } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark"]);
308
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768 } = props, runtimeOptions = __rest(props, ["emptyMessage", "hideToolCalls", "dark", "maxWidth"]);
299
309
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = (0, use_pikku_agent_runtime_js_1.usePikkuAgentRuntime)(runtimeOptions);
300
310
  const colors = dark ? darkColors : lightColors;
301
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: {
@@ -313,7 +323,7 @@ function PikkuAgentChat(props) {
313
323
  minHeight: 0,
314
324
  overflowY: 'auto',
315
325
  }, children: (0, jsx_runtime_1.jsxs)("div", { style: {
316
- maxWidth: 768,
326
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
317
327
  margin: '0 auto',
318
328
  padding: 16,
319
329
  display: 'flex',
@@ -332,5 +342,5 @@ function PikkuAgentChat(props) {
332
342
  : 'Start a new conversation.') }) }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
333
343
  UserMessage,
334
344
  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 }) })] }) }) }) }) }) }));
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 }) })] }) }) }) }) }) }));
336
346
  }
@@ -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' };
@@ -295,22 +327,56 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
295
327
  }
296
328
  }
297
329
  }
298
- const response = yield __await(fetch(`${opts.api}/${opts.agentName}/stream`, {
299
- method: 'POST',
300
- headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
301
- body: JSON.stringify({
302
- agentName: opts.agentName,
303
- message: messageText,
304
- threadId: opts.threadId,
305
- resourceId: opts.resourceId,
306
- model: opts.model,
307
- temperature: opts.temperature,
308
- }),
309
- signal: abortSignal,
310
- credentials: opts.credentials,
311
- }));
330
+ let response;
331
+ try {
332
+ response = yield __await(fetch(`${opts.api}/${opts.agentName}/stream`, {
333
+ method: 'POST',
334
+ headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
335
+ body: JSON.stringify({
336
+ agentName: opts.agentName,
337
+ message: messageText,
338
+ threadId: opts.threadId,
339
+ resourceId: opts.resourceId,
340
+ model: opts.model,
341
+ temperature: opts.temperature,
342
+ }),
343
+ signal: abortSignal,
344
+ credentials: opts.credentials,
345
+ }));
346
+ }
347
+ catch (e) {
348
+ const msg = (e === null || e === void 0 ? void 0 : e.message) || 'Unknown error';
349
+ let errorText = 'Failed to connect to the agent.';
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.';
355
+ }
356
+ else if (msg.includes('abort')) {
357
+ return yield __await(void 0);
358
+ }
359
+ yield yield __await({ content: [{ type: 'text', text: `⚠️ ${errorText}` }] });
360
+ return yield __await(void 0);
361
+ }
312
362
  if (!response.ok || !response.body) {
313
- throw new Error(`Agent stream failed: ${response.status}`);
363
+ let errorText = `Request failed (${response.status}).`;
364
+ if (response.status === 401 || response.status === 403) {
365
+ errorText = 'Authentication error.';
366
+ }
367
+ else if (response.status === 404) {
368
+ errorText =
369
+ 'Agent not found — the agent may not be configured for this project.';
370
+ }
371
+ else if (response.status === 502 || response.status === 503) {
372
+ errorText =
373
+ 'The agent server is currently unavailable. Try again in a moment.';
374
+ }
375
+ else if (response.status === 429) {
376
+ errorText = 'Rate limited — too many requests. Please wait a moment.';
377
+ }
378
+ yield yield __await({ content: [{ type: 'text', text: `⚠️ ${errorText}` }] });
379
+ return yield __await(void 0);
314
380
  }
315
381
  const text = { value: '' };
316
382
  const toolCalls = [];
@@ -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';
@@ -7,5 +7,7 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
7
7
  */
8
8
  hideToolCalls?: boolean | string[];
9
9
  dark?: boolean;
10
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
11
+ maxWidth?: number | 'none';
10
12
  }
11
13
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -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,6 +40,8 @@ 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);
@@ -156,7 +160,13 @@ const ToolCallDisplay = ({ toolCallId, toolName, args, result, status, addResult
156
160
  borderRadius: 3,
157
161
  background: colors.errorBg,
158
162
  color: colors.errorColor,
159
- }, children: "error" })), status.type === 'denied' && (_jsx("span", { style: {
163
+ }, children: "error" })), status.type === 'missing-credential' && (_jsx("span", { style: {
164
+ fontSize: 11,
165
+ padding: '1px 5px',
166
+ borderRadius: 3,
167
+ background: colors.warningBg,
168
+ color: colors.warningColor,
169
+ }, children: "credential required" })), status.type === 'denied' && (_jsx("span", { style: {
160
170
  fontSize: 11,
161
171
  padding: '1px 5px',
162
172
  borderRadius: 3,
@@ -290,7 +300,7 @@ const PikkuComposer = ({ disabled, }) => {
290
300
  }, children: "\u25B6" })] }) }) }));
291
301
  };
292
302
  export function PikkuAgentChat(props) {
293
- const { emptyMessage, hideToolCalls, dark, ...runtimeOptions } = props;
303
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, ...runtimeOptions } = props;
294
304
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
295
305
  const colors = dark ? darkColors : lightColors;
296
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: {
@@ -308,7 +318,7 @@ export function PikkuAgentChat(props) {
308
318
  minHeight: 0,
309
319
  overflowY: 'auto',
310
320
  }, children: _jsxs("div", { style: {
311
- maxWidth: 768,
321
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
312
322
  margin: '0 auto',
313
323
  padding: 16,
314
324
  display: 'flex',
@@ -328,5 +338,5 @@ export function PikkuAgentChat(props) {
328
338
  : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
329
339
  UserMessage,
330
340
  AssistantMessage,
331
- } })] }) }), _jsx("div", { style: { maxWidth: 768, margin: '0 auto', width: '100%', padding: '0 16px' }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }));
341
+ } })] }) }), _jsx("div", { style: { maxWidth: maxWidth === 'none' ? undefined : maxWidth, margin: '0 auto', width: '100%', padding: '0 16px' }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }));
332
342
  }
@@ -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' };
@@ -243,22 +275,56 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
243
275
  }
244
276
  }
245
277
  }
246
- const response = await fetch(`${opts.api}/${opts.agentName}/stream`, {
247
- method: 'POST',
248
- headers: { 'Content-Type': 'application/json', ...opts.headers },
249
- body: JSON.stringify({
250
- agentName: opts.agentName,
251
- message: messageText,
252
- threadId: opts.threadId,
253
- resourceId: opts.resourceId,
254
- model: opts.model,
255
- temperature: opts.temperature,
256
- }),
257
- signal: abortSignal,
258
- credentials: opts.credentials,
259
- });
278
+ let response;
279
+ try {
280
+ response = await fetch(`${opts.api}/${opts.agentName}/stream`, {
281
+ method: 'POST',
282
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
283
+ body: JSON.stringify({
284
+ agentName: opts.agentName,
285
+ message: messageText,
286
+ threadId: opts.threadId,
287
+ resourceId: opts.resourceId,
288
+ model: opts.model,
289
+ temperature: opts.temperature,
290
+ }),
291
+ signal: abortSignal,
292
+ credentials: opts.credentials,
293
+ });
294
+ }
295
+ catch (e) {
296
+ const msg = e?.message || 'Unknown error';
297
+ let errorText = 'Failed to connect to the agent.';
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.';
303
+ }
304
+ else if (msg.includes('abort')) {
305
+ return;
306
+ }
307
+ yield { content: [{ type: 'text', text: `⚠️ ${errorText}` }] };
308
+ return;
309
+ }
260
310
  if (!response.ok || !response.body) {
261
- throw new Error(`Agent stream failed: ${response.status}`);
311
+ let errorText = `Request failed (${response.status}).`;
312
+ if (response.status === 401 || response.status === 403) {
313
+ errorText = 'Authentication error.';
314
+ }
315
+ else if (response.status === 404) {
316
+ errorText =
317
+ 'Agent not found — the agent may not be configured for this project.';
318
+ }
319
+ else if (response.status === 502 || response.status === 503) {
320
+ errorText =
321
+ 'The agent server is currently unavailable. Try again in a moment.';
322
+ }
323
+ else if (response.status === 429) {
324
+ errorText = 'Rate limited — too many requests. Please wait a moment.';
325
+ }
326
+ yield { content: [{ type: 'text', text: `⚠️ ${errorText}` }] };
327
+ return;
262
328
  }
263
329
  const text = { value: '' };
264
330
  const toolCalls = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,8 @@
16
16
  "tsc": "tsc",
17
17
  "build:esm": "tsc -b && echo '{\"type\": \"module\"}' > dist/esm/package.json",
18
18
  "build:cjs": "tsc -b tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
19
- "build": "yarn build:esm && yarn build:cjs"
19
+ "build": "yarn build:esm && yarn build:cjs",
20
+ "prepublishOnly": "yarn build"
20
21
  },
21
22
  "dependencies": {
22
23
  "@assistant-ui/react": "^0.12.12",
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'
@@ -23,6 +23,8 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
23
23
  */
24
24
  hideToolCalls?: boolean | string[]
25
25
  dark?: boolean
26
+ /** Max width of the chat content area. Defaults to 768. Set to 'none' for full width. */
27
+ maxWidth?: number | 'none'
26
28
  }
27
29
 
28
30
  interface ChatColors {
@@ -42,6 +44,8 @@ interface ChatColors {
42
44
  successColor: string
43
45
  errorBg: string
44
46
  errorColor: string
47
+ warningBg: string
48
+ warningColor: string
45
49
  }
46
50
 
47
51
  const lightColors: ChatColors = {
@@ -61,6 +65,8 @@ const lightColors: ChatColors = {
61
65
  successColor: '#2e7d32',
62
66
  errorBg: '#ffebee',
63
67
  errorColor: '#c62828',
68
+ warningBg: '#fff3e0',
69
+ warningColor: '#e65100',
64
70
  }
65
71
 
66
72
  const darkColors: ChatColors = {
@@ -80,6 +86,8 @@ const darkColors: ChatColors = {
80
86
  successColor: '#00e68a',
81
87
  errorBg: 'rgba(220, 38, 38, 0.1)',
82
88
  errorColor: '#f87171',
89
+ warningBg: 'rgba(245, 158, 11, 0.1)',
90
+ warningColor: '#fbbf24',
83
91
  }
84
92
 
85
93
  const ColorsContext = createContext<ChatColors>(lightColors)
@@ -284,6 +292,19 @@ const ToolCallDisplay: FunctionComponent<{
284
292
  error
285
293
  </span>
286
294
  )}
295
+ {status.type === 'missing-credential' && (
296
+ <span
297
+ style={{
298
+ fontSize: 11,
299
+ padding: '1px 5px',
300
+ borderRadius: 3,
301
+ background: colors.warningBg,
302
+ color: colors.warningColor,
303
+ }}
304
+ >
305
+ credential required
306
+ </span>
307
+ )}
287
308
  {status.type === 'denied' && (
288
309
  <span
289
310
  style={{
@@ -571,7 +592,7 @@ const PikkuComposer: FunctionComponent<{ disabled?: boolean }> = ({
571
592
  }
572
593
 
573
594
  export function PikkuAgentChat(props: PikkuAgentChatProps) {
574
- const { emptyMessage, hideToolCalls, dark, ...runtimeOptions } = props
595
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, ...runtimeOptions } = props
575
596
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } =
576
597
  usePikkuAgentRuntime(runtimeOptions)
577
598
 
@@ -607,7 +628,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
607
628
  >
608
629
  <div
609
630
  style={{
610
- maxWidth: 768,
631
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
611
632
  margin: '0 auto',
612
633
  padding: 16,
613
634
  display: 'flex',
@@ -641,7 +662,7 @@ export function PikkuAgentChat(props: PikkuAgentChatProps) {
641
662
  />
642
663
  </div>
643
664
  </ThreadPrimitive.Viewport>
644
- <div style={{ maxWidth: 768, margin: '0 auto', width: '100%', padding: '0 16px' }}>
665
+ <div style={{ maxWidth: maxWidth === 'none' ? undefined : maxWidth, margin: '0 auto', width: '100%', padding: '0 16px' }}>
645
666
  <PikkuComposer disabled={isAwaitingApproval} />
646
667
  </div>
647
668
  </ThreadPrimitive.Root>
@@ -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' }
@@ -351,23 +399,54 @@ function createPikkuStreamingAdapter(
351
399
  }
352
400
  }
353
401
 
354
- const response = await fetch(`${opts.api}/${opts.agentName}/stream`, {
355
- method: 'POST',
356
- headers: { 'Content-Type': 'application/json', ...opts.headers },
357
- body: JSON.stringify({
358
- agentName: opts.agentName,
359
- message: messageText,
360
- threadId: opts.threadId,
361
- resourceId: opts.resourceId,
362
- model: opts.model,
363
- temperature: opts.temperature,
364
- }),
365
- signal: abortSignal,
366
- credentials: opts.credentials,
367
- })
402
+ let response: Response
403
+ try {
404
+ response = await fetch(`${opts.api}/${opts.agentName}/stream`, {
405
+ method: 'POST',
406
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
407
+ body: JSON.stringify({
408
+ agentName: opts.agentName,
409
+ message: messageText,
410
+ threadId: opts.threadId,
411
+ resourceId: opts.resourceId,
412
+ model: opts.model,
413
+ temperature: opts.temperature,
414
+ }),
415
+ signal: abortSignal,
416
+ credentials: opts.credentials,
417
+ })
418
+ } catch (e: any) {
419
+ const msg = e?.message || 'Unknown error'
420
+ let errorText = 'Failed to connect to the agent.'
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.'
428
+ } else if (msg.includes('abort')) {
429
+ return
430
+ }
431
+ yield { content: [{ type: 'text' as const, text: `⚠️ ${errorText}` }] }
432
+ return
433
+ }
368
434
 
369
435
  if (!response.ok || !response.body) {
370
- throw new Error(`Agent stream failed: ${response.status}`)
436
+ let errorText = `Request failed (${response.status}).`
437
+ if (response.status === 401 || response.status === 403) {
438
+ errorText = 'Authentication error.'
439
+ } else if (response.status === 404) {
440
+ errorText =
441
+ 'Agent not found — the agent may not be configured for this project.'
442
+ } else if (response.status === 502 || response.status === 503) {
443
+ errorText =
444
+ 'The agent server is currently unavailable. Try again in a moment.'
445
+ } else if (response.status === 429) {
446
+ errorText = 'Rate limited — too many requests. Please wait a moment.'
447
+ }
448
+ yield { content: [{ type: 'text' as const, text: `⚠️ ${errorText}` }] }
449
+ return
371
450
  }
372
451
 
373
452
  const text = { value: '' }