@pikku/assistant-ui 0.12.4 → 0.12.6

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.
@@ -73,7 +73,7 @@ function parseSSEStream(reader) {
73
73
  * Shared helper: consume an SSE stream and populate text/toolCalls.
74
74
  * Returns an array of PendingApprovals when the stream requests them, or empty when done.
75
75
  */
76
- function processStream(reader, text, toolCalls, yieldContent, onFinish) {
76
+ function processStream(reader, text, toolCalls, structuredParts, yieldContent, onFinish) {
77
77
  return __awaiter(this, void 0, void 0, function* () {
78
78
  var _a, e_1, _b, _c;
79
79
  const pendingApprovals = [];
@@ -122,6 +122,31 @@ function processStream(reader, text, toolCalls, yieldContent, onFinish) {
122
122
  }
123
123
  break;
124
124
  }
125
+ case 'generative-ui': {
126
+ const nextPart = {
127
+ type: 'generative-ui',
128
+ spec: event.spec,
129
+ };
130
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'generative-ui');
131
+ if (existingIndex === -1)
132
+ structuredParts.push(nextPart);
133
+ else
134
+ structuredParts[existingIndex] = nextPart;
135
+ break;
136
+ }
137
+ case 'data': {
138
+ const nextPart = {
139
+ type: 'data',
140
+ name: event.name,
141
+ data: event.data,
142
+ };
143
+ const existingIndex = structuredParts.findIndex((part) => part.type === 'data' && part.name === event.name);
144
+ if (existingIndex === -1)
145
+ structuredParts.push(nextPart);
146
+ else
147
+ structuredParts[existingIndex] = nextPart;
148
+ break;
149
+ }
125
150
  case 'approval-request':
126
151
  pendingApprovals.push({
127
152
  toolCallId: event.toolCallId,
@@ -205,13 +230,32 @@ function resolvePikkuToolStatus(status, result) {
205
230
  return { type: 'error' };
206
231
  return { type: 'completed' };
207
232
  }
208
- function buildContent(text, toolCalls) {
233
+ function buildRichContent(text, structuredParts, toolCalls) {
209
234
  const content = [];
210
235
  if (text.value)
211
236
  content.push({ type: 'text', text: text.value });
237
+ content.push(...structuredParts);
212
238
  content.push(...toolCalls);
213
239
  return content;
214
240
  }
241
+ function buildContentFromAgentResult(result) {
242
+ const content = [];
243
+ if (typeof result === 'string') {
244
+ if (result)
245
+ content.push({ type: 'text', text: result });
246
+ return content;
247
+ }
248
+ if (!result || typeof result !== 'object')
249
+ return content;
250
+ const record = result;
251
+ if (typeof record.text === 'string' && record.text) {
252
+ content.push({ type: 'text', text: record.text });
253
+ }
254
+ if (record.ui != null) {
255
+ content.push({ type: 'generative-ui', spec: record.ui });
256
+ }
257
+ return content;
258
+ }
215
259
  function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDecisionsRef, setPendingApprovalsRef, onFinishRef) {
216
260
  return {
217
261
  run(_a) {
@@ -237,6 +281,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
237
281
  // The last resume triggers continuation (next LLM step).
238
282
  let lastText = { value: '' };
239
283
  let lastToolCalls = [];
284
+ let lastStructuredParts = [];
240
285
  let nextApprovals = [];
241
286
  for (let i = 0; i < decisions.length; i++) {
242
287
  const decision = decisions[i];
@@ -262,8 +307,9 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
262
307
  }
263
308
  const text = { value: '' };
264
309
  const toolCalls = [];
310
+ const structuredParts = [];
265
311
  const reader = resumeResponse.body.getReader();
266
- const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
312
+ const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
267
313
  ? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
268
314
  : undefined)
269
315
  // Keep the last resume's output (it has continuation content)
@@ -271,12 +317,13 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
271
317
  // Keep the last resume's output (it has continuation content)
272
318
  lastText = text;
273
319
  lastToolCalls = toolCalls;
320
+ lastStructuredParts = structuredParts;
274
321
  if (streamApprovals.length > 0) {
275
322
  nextApprovals = streamApprovals;
276
323
  }
277
324
  }
278
325
  // Build content from the last resume's output
279
- const content = buildContent(lastText, lastToolCalls);
326
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
280
327
  if (nextApprovals.length > 0) {
281
328
  // More approvals from continuation — show them
282
329
  pendingApprovalsRef.current = nextApprovals;
@@ -301,7 +348,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
301
348
  lastToolCalls.push(approvalToolCall);
302
349
  }
303
350
  }
304
- const updatedContent = buildContent(lastText, lastToolCalls);
351
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
305
352
  yield yield __await({
306
353
  content: updatedContent,
307
354
  status: {
@@ -332,14 +379,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
332
379
  response = yield __await(fetch(`${opts.api}/${opts.agentName}/stream`, {
333
380
  method: 'POST',
334
381
  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
- }),
382
+ body: JSON.stringify(Object.assign({ agentName: opts.agentName, message: messageText, threadId: opts.threadId, resourceId: opts.resourceId, model: opts.model, temperature: opts.temperature }, (opts.context ? { context: opts.context } : {}))),
343
383
  signal: abortSignal,
344
384
  credentials: opts.credentials,
345
385
  }));
@@ -380,14 +420,15 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
380
420
  }
381
421
  const text = { value: '' };
382
422
  const toolCalls = [];
423
+ const structuredParts = [];
383
424
  let pendingContent = null;
384
425
  const yieldContent = () => {
385
- const content = buildContent(text, toolCalls);
426
+ const content = buildRichContent(text, structuredParts, toolCalls);
386
427
  if (content.length > 0)
387
428
  pendingContent = content;
388
429
  };
389
430
  const reader = response.body.getReader();
390
- const approvals = yield __await(processStream(reader, text, toolCalls, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
431
+ const approvals = yield __await(processStream(reader, text, toolCalls, structuredParts, yieldContent, (_e = onFinishRef.current) !== null && _e !== void 0 ? _e : undefined));
391
432
  if (approvals.length === 0) {
392
433
  // No approval needed — yield final content and done
393
434
  if (pendingContent) {
@@ -427,7 +468,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
427
468
  toolCalls.splice(i, 1);
428
469
  }
429
470
  }
430
- const content = buildContent(text, toolCalls);
471
+ const content = buildRichContent(text, structuredParts, toolCalls);
431
472
  yield yield __await({
432
473
  content,
433
474
  status: {
@@ -497,7 +538,12 @@ const convertDbMessages = (dbMessages) => {
497
538
  continue;
498
539
  const parts = [];
499
540
  if (msg.content) {
500
- parts.push({ type: 'text', text: msg.content });
541
+ if (Array.isArray(msg.content)) {
542
+ parts.push(...msg.content);
543
+ }
544
+ else {
545
+ parts.push({ type: 'text', text: msg.content });
546
+ }
501
547
  }
502
548
  if (Array.isArray(msg.toolCalls)) {
503
549
  for (const tc of msg.toolCalls) {
@@ -587,6 +633,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
587
633
  // Resume uses SSE (same as streaming mode)
588
634
  let lastText = { value: '' };
589
635
  let lastToolCalls = [];
636
+ let lastStructuredParts = [];
590
637
  let nextApprovals = [];
591
638
  for (let i = 0; i < decisions.length; i++) {
592
639
  const decision = decisions[i];
@@ -611,17 +658,19 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
611
658
  }
612
659
  const text = { value: '' };
613
660
  const toolCalls = [];
661
+ const structuredParts = [];
614
662
  const reader = resumeResponse.body.getReader();
615
- const streamApprovals = yield __await(processStream(reader, text, toolCalls, () => { }, i === decisions.length - 1
663
+ const streamApprovals = yield __await(processStream(reader, text, toolCalls, structuredParts, () => { }, i === decisions.length - 1
616
664
  ? ((_d = onFinishRef.current) !== null && _d !== void 0 ? _d : undefined)
617
665
  : undefined));
618
666
  lastText = text;
619
667
  lastToolCalls = toolCalls;
668
+ lastStructuredParts = structuredParts;
620
669
  if (streamApprovals.length > 0) {
621
670
  nextApprovals = streamApprovals;
622
671
  }
623
672
  }
624
- const content = buildContent(lastText, lastToolCalls);
673
+ const content = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
625
674
  if (nextApprovals.length > 0) {
626
675
  pendingApprovalsRef.current = nextApprovals;
627
676
  setPendingApprovalsRef.current(nextApprovals);
@@ -644,7 +693,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
644
693
  lastToolCalls.push(approvalToolCall);
645
694
  }
646
695
  }
647
- const updatedContent = buildContent(lastText, lastToolCalls);
696
+ const updatedContent = buildRichContent(lastText, lastStructuredParts, lastToolCalls);
648
697
  yield yield __await({
649
698
  content: updatedContent,
650
699
  status: {
@@ -673,14 +722,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
673
722
  const response = yield __await(fetch(`${opts.api}/${opts.agentName}`, {
674
723
  method: 'POST',
675
724
  headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
676
- body: JSON.stringify({
677
- agentName: opts.agentName,
678
- message: messageText,
679
- threadId: opts.threadId,
680
- resourceId: opts.resourceId,
681
- model: opts.model,
682
- temperature: opts.temperature,
683
- }),
725
+ body: JSON.stringify(Object.assign({ agentName: opts.agentName, message: messageText, threadId: opts.threadId, resourceId: opts.resourceId, model: opts.model, temperature: opts.temperature }, (opts.context ? { context: opts.context } : {}))),
684
726
  signal: abortSignal,
685
727
  credentials: opts.credentials,
686
728
  }));
@@ -701,9 +743,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
701
743
  : {})), (approval.reason ? { __approvalReason: approval.reason } : {})),
702
744
  }));
703
745
  const content = [];
704
- if (json.result) {
705
- content.push({ type: 'text', text: json.result });
706
- }
746
+ content.push(...buildContentFromAgentResult(json.result));
707
747
  content.push(...toolCalls);
708
748
  yield yield __await({
709
749
  content,
@@ -716,10 +756,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
716
756
  }
717
757
  // No approvals — yield complete content
718
758
  (_f = onFinishRef.current) === null || _f === void 0 ? void 0 : _f.call(onFinishRef);
719
- const content = [];
720
- if (json.result) {
721
- content.push({ type: 'text', text: String(json.result) });
722
- }
759
+ const content = buildContentFromAgentResult(json.result);
723
760
  if (content.length > 0) {
724
761
  yield yield __await({ content });
725
762
  }
@@ -2,3 +2,6 @@ export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalCo
2
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';
5
+ export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js';
6
+ export type { PendingFile, UploadAttachmentFn } from './use-file-attachment.js';
7
+ export { modelSupportsVision } from './model-capabilities.js';
package/dist/esm/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { usePikkuAgentRuntime, usePikkuAgentNonStreamingRuntime, PikkuApprovalContext, usePikkuApproval, convertDbMessages, isDeniedResult, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
2
2
  export { PikkuAgentChat } from './pikku-agent-chat.js';
3
+ export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js';
4
+ export { modelSupportsVision } from './model-capabilities.js';
@@ -0,0 +1 @@
1
+ export declare function modelSupportsVision(modelID: string): boolean;
@@ -0,0 +1,13 @@
1
+ const MODEL_VISION_SUPPORT = new Set([
2
+ 'claude-sonnet-4-5',
3
+ 'claude-opus-4-1',
4
+ 'gpt-4o-mini',
5
+ 'gpt-5',
6
+ 'gpt-5-mini',
7
+ 'gpt-5-codex',
8
+ 'gemini-2.5-pro',
9
+ 'gemini-2.5-flash',
10
+ ]);
11
+ export function modelSupportsVision(modelID) {
12
+ return MODEL_VISION_SUPPORT.has(modelID);
13
+ }
@@ -1,6 +1,8 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
1
2
  import { type ToolCallMessagePartComponent } from '@assistant-ui/react';
2
3
  import { type PikkuAgentRuntimeOptions } from './use-pikku-agent-runtime.js';
3
4
  export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
5
+ initialPrompt?: string;
4
6
  emptyMessage?: string;
5
7
  /** Hide tool calls from the chat display.
6
8
  * - `true`: hide all non-approval tool calls
@@ -21,5 +23,7 @@ export interface PikkuAgentChatProps extends PikkuAgentRuntimeOptions {
21
23
  * (which still respects `hideToolCalls` and the approval-request UI).
22
24
  */
23
25
  toolComponents?: Record<string, ToolCallMessagePartComponent>;
26
+ renderAssistantText?: (text: string) => ReactNode;
27
+ generativeUIComponents?: Record<string, ComponentType<any>>;
24
28
  }
25
29
  export declare function PikkuAgentChat(props: PikkuAgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useMemo, } from 'react';
2
+ import { createContext, useContext, useState, useMemo, useEffect, useRef, } from 'react';
3
3
  import Markdown from 'react-markdown';
4
- import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, } from '@assistant-ui/react';
4
+ import { AssistantRuntimeProvider, ThreadPrimitive, MessagePrimitive, ComposerPrimitive, useComposerRuntime, } from '@assistant-ui/react';
5
5
  import { usePikkuAgentRuntime, PikkuApprovalContext, usePikkuApproval, resolvePikkuToolStatus, } from './use-pikku-agent-runtime.js';
6
6
  const lightColors = {
7
7
  bg: '#ffffff',
@@ -46,6 +46,8 @@ const darkColors = {
46
46
  const ColorsContext = createContext(lightColors);
47
47
  const HideToolCallsContext = createContext(undefined);
48
48
  const ToolComponentsContext = createContext(undefined);
49
+ const GenerativeUIComponentsContext = createContext(undefined);
50
+ const RenderAssistantTextContext = createContext(undefined);
49
51
  function shouldHideToolCall(hideToolCalls, toolName) {
50
52
  if (!hideToolCalls)
51
53
  return false;
@@ -296,6 +298,8 @@ const UserMessage = () => {
296
298
  const AssistantMessage = () => {
297
299
  const colors = useContext(ColorsContext);
298
300
  const toolComponents = useContext(ToolComponentsContext);
301
+ const generativeUIComponents = useContext(GenerativeUIComponentsContext);
302
+ const renderAssistantText = useContext(RenderAssistantTextContext);
299
303
  return (_jsx("div", { style: {
300
304
  display: 'flex',
301
305
  justifyContent: 'flex-start',
@@ -305,11 +309,18 @@ const AssistantMessage = () => {
305
309
  borderRadius: 12,
306
310
  backgroundColor: colors.assistantBubble,
307
311
  }, children: [_jsx(MessagePrimitive.Content, { components: {
308
- Text: ({ text }) => _jsx(MarkdownText, { text: text, colors: colors }),
312
+ Text: ({ text }) => renderAssistantText ? (_jsx(_Fragment, { children: renderAssistantText(text) })) : (_jsx(MarkdownText, { text: text, colors: colors })),
309
313
  tools: {
310
314
  by_name: toolComponents,
311
315
  Fallback: (props) => (_jsx(ToolCallDisplay, { toolCallId: props.toolCallId, toolName: props.toolName, args: props.args, result: props.result, status: resolvePikkuToolStatus(props.status, props.result), addResult: props.addResult })),
312
316
  },
317
+ ...(generativeUIComponents
318
+ ? {
319
+ generativeUI: {
320
+ components: generativeUIComponents,
321
+ },
322
+ }
323
+ : {}),
313
324
  } }), _jsx(MessagePrimitive.If, { last: true, children: _jsx(ThreadPrimitive.If, { running: true, children: _jsx("div", { style: {
314
325
  display: 'flex',
315
326
  alignItems: 'center',
@@ -319,88 +330,112 @@ const AssistantMessage = () => {
319
330
  color: colors.textMuted,
320
331
  }, children: "Thinking..." }) }) })] })] }) }));
321
332
  };
333
+ const ComposerPrefill = ({ text }) => {
334
+ const composer = useComposerRuntime();
335
+ const filled = useRef(false);
336
+ useEffect(() => {
337
+ if (filled.current || !text)
338
+ return;
339
+ filled.current = true;
340
+ if (composer.getState().text === '')
341
+ composer.setText(text);
342
+ }, [text, composer]);
343
+ return null;
344
+ };
322
345
  const PikkuComposer = ({ disabled, }) => {
323
346
  const colors = useContext(ColorsContext);
324
347
  return (_jsx("div", { style: { padding: '8px 0 16px' }, children: _jsx(ComposerPrimitive.Root, { children: _jsxs("div", { style: {
325
- border: `1px solid ${colors.border}`,
326
- borderRadius: 12,
327
- overflow: 'hidden',
348
+ position: 'relative',
349
+ zIndex: 2,
328
350
  display: 'flex',
329
- alignItems: 'flex-end',
330
- padding: '6px 12px',
331
- gap: 8,
351
+ flexDirection: 'column',
352
+ gap: 10,
353
+ backgroundColor: colors.assistantBubble,
354
+ border: `1px solid ${colors.border}`,
355
+ borderRadius: 24,
356
+ padding: '14px 12px 10px',
357
+ boxShadow: darkColors.text === colors.text
358
+ ? '0 14px 30px rgba(0,0,0,0.24)'
359
+ : '0 14px 30px rgba(0,0,0,0.08)',
332
360
  ...(disabled
333
361
  ? { opacity: 0.5, pointerEvents: 'none' }
334
362
  : {}),
335
- }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 2, disabled: disabled, style: {
336
- flex: 1,
363
+ }, children: [_jsx(ComposerPrimitive.Input, { placeholder: disabled ? 'Respond to approval request above...' : 'Message...', rows: 1, disabled: disabled ?? false, style: {
364
+ width: '100%',
365
+ background: 'transparent',
337
366
  border: 'none',
338
367
  outline: 'none',
339
- resize: 'none',
368
+ color: colors.text,
340
369
  fontSize: 14,
341
370
  fontFamily: 'inherit',
342
- padding: '4px 0',
343
- background: colors.inputBg,
344
- color: colors.text,
345
- } }), _jsx(ComposerPrimitive.Send, { disabled: disabled, style: {
346
- width: 28,
347
- height: 28,
348
- borderRadius: '50%',
349
- border: 'none',
350
- background: disabled ? colors.textMuted : colors.sendBg,
351
- color: colors.sendColor,
352
- cursor: disabled ? 'not-allowed' : 'pointer',
371
+ resize: 'none',
372
+ lineHeight: 1.5,
373
+ overflowY: 'auto',
374
+ } }), _jsx("div", { style: {
375
+ width: '100%',
353
376
  display: 'flex',
354
377
  alignItems: 'center',
355
- justifyContent: 'center',
356
- flexShrink: 0,
357
- marginBottom: 2,
358
- fontSize: 14,
359
- }, children: "\u25B6" })] }) }) }));
378
+ gap: 6,
379
+ minWidth: 0,
380
+ justifyContent: 'flex-end',
381
+ }, children: _jsx(ComposerPrimitive.Send, { disabled: disabled ?? false, style: {
382
+ width: 30,
383
+ height: 30,
384
+ borderRadius: 999,
385
+ border: `1px solid ${colors.border}`,
386
+ background: '#e8e8e8',
387
+ color: '#111111',
388
+ cursor: disabled ? 'not-allowed' : 'pointer',
389
+ display: 'flex',
390
+ alignItems: 'center',
391
+ justifyContent: 'center',
392
+ flexShrink: 0,
393
+ transition: 'background-color 150ms ease, color 150ms ease, border-color 150ms ease',
394
+ }, children: "\u2191" }) })] }) }) }));
360
395
  };
361
396
  export function PikkuAgentChat(props) {
362
- const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, ...runtimeOptions } = props;
397
+ const { emptyMessage, hideToolCalls, dark, maxWidth = 768, toolComponents, renderAssistantText, generativeUIComponents, initialPrompt, ...runtimeOptions } = props;
363
398
  const { runtime, isAwaitingApproval, pendingApprovals, handleApproval } = usePikkuAgentRuntime(runtimeOptions);
364
399
  const colors = dark ? darkColors : lightColors;
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%',
367
- display: 'flex',
368
- flexDirection: 'column',
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,
400
+ return (_jsx(ColorsContext.Provider, { value: colors, children: _jsx(PikkuApprovalContext.Provider, { value: { pendingApprovals, handleApproval }, children: _jsx(HideToolCallsContext.Provider, { value: hideToolCalls, children: _jsx(ToolComponentsContext.Provider, { value: toolComponents, children: _jsx(GenerativeUIComponentsContext.Provider, { value: generativeUIComponents, children: _jsx(RenderAssistantTextContext.Provider, { value: renderAssistantText, children: _jsxs(AssistantRuntimeProvider, { runtime: runtime, children: [_jsx(ComposerPrefill, { text: initialPrompt }), _jsx("div", { style: {
401
+ height: '100%',
402
+ display: 'flex',
403
+ flexDirection: 'column',
404
+ background: colors.bg,
405
+ }, children: _jsxs(ThreadPrimitive.Root, { style: {
383
406
  display: 'flex',
384
407
  flexDirection: 'column',
385
- gap: 16,
386
- }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
408
+ flex: 1,
409
+ minHeight: 0,
410
+ }, children: [_jsx(ThreadPrimitive.Viewport, { style: {
411
+ flex: 1,
412
+ minHeight: 0,
413
+ overflowY: 'auto',
414
+ }, children: _jsxs("div", { style: {
415
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
416
+ margin: '0 auto',
417
+ padding: 16,
387
418
  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,
402
- margin: '0 auto',
403
- width: '100%',
404
- padding: '0 16px',
405
- }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) }) }) }) }) }) }));
419
+ flexDirection: 'column',
420
+ gap: 16,
421
+ }, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { style: {
422
+ display: 'flex',
423
+ alignItems: 'center',
424
+ justifyContent: 'center',
425
+ minHeight: 300,
426
+ color: colors.textMuted,
427
+ textAlign: 'center',
428
+ fontSize: 14,
429
+ }, children: emptyMessage ??
430
+ (props.threadId
431
+ ? 'Send a message to start the conversation.'
432
+ : 'Start a new conversation.') }) }), _jsx(ThreadPrimitive.Messages, { components: {
433
+ UserMessage,
434
+ AssistantMessage,
435
+ } })] }) }), _jsx("div", { style: {
436
+ maxWidth: maxWidth === 'none' ? undefined : maxWidth,
437
+ margin: '0 auto',
438
+ width: '100%',
439
+ padding: '0 16px',
440
+ }, children: _jsx(PikkuComposer, { disabled: isAwaitingApproval }) })] }) })] }) }) }) }) }) }) }));
406
441
  }
@@ -0,0 +1,26 @@
1
+ export type PendingFile = {
2
+ id: string;
3
+ name: string;
4
+ mimeType: string;
5
+ previewUrl: string;
6
+ contentUrl: string;
7
+ isImage: boolean;
8
+ };
9
+ export type UploadAttachmentFn = (args: {
10
+ contentType: string;
11
+ sizeBytes: number;
12
+ }) => Promise<{
13
+ uploadUrl: string;
14
+ signedReadUrl: string;
15
+ uploadMethod?: string;
16
+ }>;
17
+ export declare const INLINE_SIZE_LIMIT: number;
18
+ export declare function useFileAttachment(upload: UploadAttachmentFn): {
19
+ pendingFiles: PendingFile[];
20
+ uploading: boolean;
21
+ uploadError: string | null;
22
+ fileInputRef: import("react").RefObject<HTMLInputElement | null>;
23
+ handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
24
+ removeFile: (id: string) => void;
25
+ clearFiles: () => void;
26
+ };
@@ -0,0 +1,85 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ export const INLINE_SIZE_LIMIT = 1 * 1024 * 1024;
3
+ export function useFileAttachment(upload) {
4
+ const [pendingFiles, setPendingFiles] = useState([]);
5
+ const [uploading, setUploading] = useState(false);
6
+ const [uploadError, setUploadError] = useState(null);
7
+ const fileInputRef = useRef(null);
8
+ const handleFileChange = useCallback(async (e) => {
9
+ const files = Array.from(e.target.files ?? []);
10
+ e.target.value = '';
11
+ if (!files.length)
12
+ return;
13
+ setUploading(true);
14
+ setUploadError(null);
15
+ try {
16
+ for (const file of files) {
17
+ const previewUrl = URL.createObjectURL(file);
18
+ let contentUrl;
19
+ if (file.type.startsWith('image/') &&
20
+ file.size <= INLINE_SIZE_LIMIT) {
21
+ contentUrl = await new Promise((resolve, reject) => {
22
+ const reader = new FileReader();
23
+ reader.onload = (ev) => resolve(ev.target.result);
24
+ reader.onerror = reject;
25
+ reader.readAsDataURL(file);
26
+ });
27
+ }
28
+ else {
29
+ const { uploadUrl, signedReadUrl, uploadMethod } = await upload({
30
+ contentType: file.type,
31
+ sizeBytes: file.size,
32
+ });
33
+ const resp = await fetch(uploadUrl, {
34
+ method: uploadMethod ?? 'PUT',
35
+ body: file,
36
+ headers: { 'Content-Type': file.type },
37
+ });
38
+ if (!resp.ok)
39
+ throw new Error(`Upload failed (${resp.status})`);
40
+ contentUrl = signedReadUrl;
41
+ }
42
+ setPendingFiles((prev) => [
43
+ ...prev,
44
+ {
45
+ id: `file_${Date.now().toString(36)}`,
46
+ name: file.name,
47
+ mimeType: file.type,
48
+ previewUrl,
49
+ contentUrl,
50
+ isImage: file.type.startsWith('image/'),
51
+ },
52
+ ]);
53
+ }
54
+ }
55
+ catch (err) {
56
+ setUploadError(err instanceof Error ? err.message : 'Upload failed');
57
+ }
58
+ finally {
59
+ setUploading(false);
60
+ }
61
+ }, [upload]);
62
+ const removeFile = useCallback((id) => {
63
+ setPendingFiles((prev) => {
64
+ const f = prev.find((x) => x.id === id);
65
+ if (f)
66
+ URL.revokeObjectURL(f.previewUrl);
67
+ return prev.filter((x) => x.id !== id);
68
+ });
69
+ }, []);
70
+ const clearFiles = useCallback(() => {
71
+ setPendingFiles((prev) => {
72
+ prev.forEach((f) => URL.revokeObjectURL(f.previewUrl));
73
+ return [];
74
+ });
75
+ }, []);
76
+ return {
77
+ pendingFiles,
78
+ uploading,
79
+ uploadError,
80
+ fileInputRef,
81
+ handleFileChange,
82
+ removeFile,
83
+ clearFiles,
84
+ };
85
+ }
@@ -10,6 +10,10 @@ export interface PikkuAgentRuntimeOptions {
10
10
  headers?: Record<string, string>;
11
11
  model?: string;
12
12
  temperature?: number;
13
+ /** Structured context injected into the agent's system instructions.
14
+ * Provide upfront state (e.g. current org/project/branch/deployment IDs)
15
+ * so the agent can call tools without asking the user. */
16
+ context?: string;
13
17
  }
14
18
  export interface PendingApproval {
15
19
  toolCallId: string;