@pikku/assistant-ui 0.12.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @pikku/assistant-ui
2
2
 
3
+ ## 0.12.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f4f7046: feat(assistant-ui): export useFileAttachment hook, modelSupportsVision, PendingFile, UploadAttachmentFn, and INLINE_SIZE_LIMIT
8
+
3
9
  ## 0.12.5
4
10
 
5
11
  ### Patch Changes
@@ -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/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PikkuAgentChat = exports.resolvePikkuToolStatus = exports.isDeniedResult = exports.convertDbMessages = exports.usePikkuApproval = exports.PikkuApprovalContext = exports.usePikkuAgentNonStreamingRuntime = exports.usePikkuAgentRuntime = void 0;
3
+ exports.modelSupportsVision = exports.INLINE_SIZE_LIMIT = exports.useFileAttachment = exports.PikkuAgentChat = exports.resolvePikkuToolStatus = exports.isDeniedResult = exports.convertDbMessages = exports.usePikkuApproval = exports.PikkuApprovalContext = exports.usePikkuAgentNonStreamingRuntime = exports.usePikkuAgentRuntime = void 0;
4
4
  var use_pikku_agent_runtime_js_1 = require("./use-pikku-agent-runtime.js");
5
5
  Object.defineProperty(exports, "usePikkuAgentRuntime", { enumerable: true, get: function () { return use_pikku_agent_runtime_js_1.usePikkuAgentRuntime; } });
6
6
  Object.defineProperty(exports, "usePikkuAgentNonStreamingRuntime", { enumerable: true, get: function () { return use_pikku_agent_runtime_js_1.usePikkuAgentNonStreamingRuntime; } });
@@ -11,3 +11,8 @@ Object.defineProperty(exports, "isDeniedResult", { enumerable: true, get: functi
11
11
  Object.defineProperty(exports, "resolvePikkuToolStatus", { enumerable: true, get: function () { return use_pikku_agent_runtime_js_1.resolvePikkuToolStatus; } });
12
12
  var pikku_agent_chat_js_1 = require("./pikku-agent-chat.js");
13
13
  Object.defineProperty(exports, "PikkuAgentChat", { enumerable: true, get: function () { return pikku_agent_chat_js_1.PikkuAgentChat; } });
14
+ var use_file_attachment_js_1 = require("./use-file-attachment.js");
15
+ Object.defineProperty(exports, "useFileAttachment", { enumerable: true, get: function () { return use_file_attachment_js_1.useFileAttachment; } });
16
+ Object.defineProperty(exports, "INLINE_SIZE_LIMIT", { enumerable: true, get: function () { return use_file_attachment_js_1.INLINE_SIZE_LIMIT; } });
17
+ var model_capabilities_js_1 = require("./model-capabilities.js");
18
+ Object.defineProperty(exports, "modelSupportsVision", { enumerable: true, get: function () { return model_capabilities_js_1.modelSupportsVision; } });
@@ -0,0 +1 @@
1
+ export declare function modelSupportsVision(modelID: string): boolean;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.modelSupportsVision = modelSupportsVision;
4
+ const MODEL_VISION_SUPPORT = new Set([
5
+ 'claude-sonnet-4-5',
6
+ 'claude-opus-4-1',
7
+ 'gpt-4o-mini',
8
+ 'gpt-5',
9
+ 'gpt-5-mini',
10
+ 'gpt-5-codex',
11
+ 'gemini-2.5-pro',
12
+ 'gemini-2.5-flash',
13
+ ]);
14
+ function modelSupportsVision(modelID) {
15
+ return MODEL_VISION_SUPPORT.has(modelID);
16
+ }
@@ -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,99 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.INLINE_SIZE_LIMIT = void 0;
13
+ exports.useFileAttachment = useFileAttachment;
14
+ const react_1 = require("react");
15
+ exports.INLINE_SIZE_LIMIT = 1 * 1024 * 1024;
16
+ function useFileAttachment(upload) {
17
+ const [pendingFiles, setPendingFiles] = (0, react_1.useState)([]);
18
+ const [uploading, setUploading] = (0, react_1.useState)(false);
19
+ const [uploadError, setUploadError] = (0, react_1.useState)(null);
20
+ const fileInputRef = (0, react_1.useRef)(null);
21
+ const handleFileChange = (0, react_1.useCallback)((e) => __awaiter(this, void 0, void 0, function* () {
22
+ var _a;
23
+ const files = Array.from((_a = e.target.files) !== null && _a !== void 0 ? _a : []);
24
+ e.target.value = '';
25
+ if (!files.length)
26
+ return;
27
+ setUploading(true);
28
+ setUploadError(null);
29
+ try {
30
+ for (const file of files) {
31
+ const previewUrl = URL.createObjectURL(file);
32
+ let contentUrl;
33
+ if (file.type.startsWith('image/') &&
34
+ file.size <= exports.INLINE_SIZE_LIMIT) {
35
+ contentUrl = yield new Promise((resolve, reject) => {
36
+ const reader = new FileReader();
37
+ reader.onload = (ev) => resolve(ev.target.result);
38
+ reader.onerror = reject;
39
+ reader.readAsDataURL(file);
40
+ });
41
+ }
42
+ else {
43
+ const { uploadUrl, signedReadUrl, uploadMethod } = yield upload({
44
+ contentType: file.type,
45
+ sizeBytes: file.size,
46
+ });
47
+ const resp = yield fetch(uploadUrl, {
48
+ method: uploadMethod !== null && uploadMethod !== void 0 ? uploadMethod : 'PUT',
49
+ body: file,
50
+ headers: { 'Content-Type': file.type },
51
+ });
52
+ if (!resp.ok)
53
+ throw new Error(`Upload failed (${resp.status})`);
54
+ contentUrl = signedReadUrl;
55
+ }
56
+ setPendingFiles((prev) => [
57
+ ...prev,
58
+ {
59
+ id: `file_${Date.now().toString(36)}`,
60
+ name: file.name,
61
+ mimeType: file.type,
62
+ previewUrl,
63
+ contentUrl,
64
+ isImage: file.type.startsWith('image/'),
65
+ },
66
+ ]);
67
+ }
68
+ }
69
+ catch (err) {
70
+ setUploadError(err instanceof Error ? err.message : 'Upload failed');
71
+ }
72
+ finally {
73
+ setUploading(false);
74
+ }
75
+ }), [upload]);
76
+ const removeFile = (0, react_1.useCallback)((id) => {
77
+ setPendingFiles((prev) => {
78
+ const f = prev.find((x) => x.id === id);
79
+ if (f)
80
+ URL.revokeObjectURL(f.previewUrl);
81
+ return prev.filter((x) => x.id !== id);
82
+ });
83
+ }, []);
84
+ const clearFiles = (0, react_1.useCallback)(() => {
85
+ setPendingFiles((prev) => {
86
+ prev.forEach((f) => URL.revokeObjectURL(f.previewUrl));
87
+ return [];
88
+ });
89
+ }, []);
90
+ return {
91
+ pendingFiles,
92
+ uploading,
93
+ uploadError,
94
+ fileInputRef,
95
+ handleFileChange,
96
+ removeFile,
97
+ clearFiles,
98
+ };
99
+ }
@@ -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;
@@ -379,14 +379,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
379
379
  response = yield __await(fetch(`${opts.api}/${opts.agentName}/stream`, {
380
380
  method: 'POST',
381
381
  headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
382
- body: JSON.stringify({
383
- agentName: opts.agentName,
384
- message: messageText,
385
- threadId: opts.threadId,
386
- resourceId: opts.resourceId,
387
- model: opts.model,
388
- temperature: opts.temperature,
389
- }),
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 } : {}))),
390
383
  signal: abortSignal,
391
384
  credentials: opts.credentials,
392
385
  }));
@@ -729,14 +722,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
729
722
  const response = yield __await(fetch(`${opts.api}/${opts.agentName}`, {
730
723
  method: 'POST',
731
724
  headers: Object.assign({ 'Content-Type': 'application/json' }, opts.headers),
732
- body: JSON.stringify({
733
- agentName: opts.agentName,
734
- message: messageText,
735
- threadId: opts.threadId,
736
- resourceId: opts.resourceId,
737
- model: opts.model,
738
- temperature: opts.temperature,
739
- }),
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 } : {}))),
740
726
  signal: abortSignal,
741
727
  credentials: opts.credentials,
742
728
  }));
@@ -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
+ }
@@ -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;
@@ -334,6 +334,7 @@ function createPikkuStreamingAdapter(optionsRef, pendingApprovalsRef, approvalDe
334
334
  resourceId: opts.resourceId,
335
335
  model: opts.model,
336
336
  temperature: opts.temperature,
337
+ ...(opts.context ? { context: opts.context } : {}),
337
338
  }),
338
339
  signal: abortSignal,
339
340
  credentials: opts.credentials,
@@ -688,6 +689,7 @@ function createPikkuNonStreamingAdapter(optionsRef, pendingApprovalsRef, approva
688
689
  resourceId: opts.resourceId,
689
690
  model: opts.model,
690
691
  temperature: opts.temperature,
692
+ ...(opts.context ? { context: opts.context } : {}),
691
693
  }),
692
694
  signal: abortSignal,
693
695
  credentials: opts.credentials,
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
1
+ {"root":["../src/index.ts","../src/model-capabilities.ts","../src/use-file-attachment.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
1
+ {"root":["../src/index.ts","../src/model-capabilities.ts","../src/use-file-attachment.ts","../src/use-pikku-agent-runtime.ts","../src/pikku-agent-chat.tsx"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/assistant-ui",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "prepublishOnly": "yarn build"
21
21
  },
22
22
  "dependencies": {
23
- "@assistant-ui/react": "^0.12.12",
23
+ "@assistant-ui/react": "^0.12.12 || ^0.14.0",
24
24
  "react-markdown": "^10.1.0"
25
25
  },
26
26
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -17,3 +17,6 @@ export type {
17
17
  } from './use-pikku-agent-runtime.js'
18
18
  export { PikkuAgentChat } from './pikku-agent-chat.js'
19
19
  export type { PikkuAgentChatProps } from './pikku-agent-chat.js'
20
+ export { useFileAttachment, INLINE_SIZE_LIMIT } from './use-file-attachment.js'
21
+ export type { PendingFile, UploadAttachmentFn } from './use-file-attachment.js'
22
+ export { modelSupportsVision } from './model-capabilities.js'
@@ -0,0 +1,14 @@
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
+
12
+ export function modelSupportsVision(modelID: string): boolean {
13
+ return MODEL_VISION_SUPPORT.has(modelID)
14
+ }
@@ -0,0 +1,110 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+
3
+ export type PendingFile = {
4
+ id: string
5
+ name: string
6
+ mimeType: string
7
+ previewUrl: string
8
+ contentUrl: string
9
+ isImage: boolean
10
+ }
11
+
12
+ export type UploadAttachmentFn = (args: {
13
+ contentType: string
14
+ sizeBytes: number
15
+ }) => Promise<{
16
+ uploadUrl: string
17
+ signedReadUrl: string
18
+ uploadMethod?: string
19
+ }>
20
+
21
+ export const INLINE_SIZE_LIMIT = 1 * 1024 * 1024
22
+
23
+ export function useFileAttachment(upload: UploadAttachmentFn) {
24
+ const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([])
25
+ const [uploading, setUploading] = useState(false)
26
+ const [uploadError, setUploadError] = useState<string | null>(null)
27
+ const fileInputRef = useRef<HTMLInputElement>(null)
28
+
29
+ const handleFileChange = useCallback(
30
+ async (e: React.ChangeEvent<HTMLInputElement>) => {
31
+ const files = Array.from(e.target.files ?? [])
32
+ e.target.value = ''
33
+ if (!files.length) return
34
+ setUploading(true)
35
+ setUploadError(null)
36
+ try {
37
+ for (const file of files) {
38
+ const previewUrl = URL.createObjectURL(file)
39
+ let contentUrl: string
40
+
41
+ if (
42
+ file.type.startsWith('image/') &&
43
+ file.size <= INLINE_SIZE_LIMIT
44
+ ) {
45
+ contentUrl = await new Promise<string>((resolve, reject) => {
46
+ const reader = new FileReader()
47
+ reader.onload = (ev) => resolve(ev.target!.result as string)
48
+ reader.onerror = reject
49
+ reader.readAsDataURL(file)
50
+ })
51
+ } else {
52
+ const { uploadUrl, signedReadUrl, uploadMethod } = await upload({
53
+ contentType: file.type,
54
+ sizeBytes: file.size,
55
+ })
56
+ const resp = await fetch(uploadUrl, {
57
+ method: uploadMethod ?? 'PUT',
58
+ body: file,
59
+ headers: { 'Content-Type': file.type },
60
+ })
61
+ if (!resp.ok) throw new Error(`Upload failed (${resp.status})`)
62
+ contentUrl = signedReadUrl
63
+ }
64
+
65
+ setPendingFiles((prev) => [
66
+ ...prev,
67
+ {
68
+ id: `file_${Date.now().toString(36)}`,
69
+ name: file.name,
70
+ mimeType: file.type,
71
+ previewUrl,
72
+ contentUrl,
73
+ isImage: file.type.startsWith('image/'),
74
+ },
75
+ ])
76
+ }
77
+ } catch (err) {
78
+ setUploadError(err instanceof Error ? err.message : 'Upload failed')
79
+ } finally {
80
+ setUploading(false)
81
+ }
82
+ },
83
+ [upload]
84
+ )
85
+
86
+ const removeFile = useCallback((id: string) => {
87
+ setPendingFiles((prev) => {
88
+ const f = prev.find((x) => x.id === id)
89
+ if (f) URL.revokeObjectURL(f.previewUrl)
90
+ return prev.filter((x) => x.id !== id)
91
+ })
92
+ }, [])
93
+
94
+ const clearFiles = useCallback(() => {
95
+ setPendingFiles((prev) => {
96
+ prev.forEach((f) => URL.revokeObjectURL(f.previewUrl))
97
+ return []
98
+ })
99
+ }, [])
100
+
101
+ return {
102
+ pendingFiles,
103
+ uploading,
104
+ uploadError,
105
+ fileInputRef,
106
+ handleFileChange,
107
+ removeFile,
108
+ clearFiles,
109
+ }
110
+ }
@@ -23,6 +23,10 @@ export interface PikkuAgentRuntimeOptions {
23
23
  headers?: Record<string, string>
24
24
  model?: string
25
25
  temperature?: number
26
+ /** Structured context injected into the agent's system instructions.
27
+ * Provide upfront state (e.g. current org/project/branch/deployment IDs)
28
+ * so the agent can call tools without asking the user. */
29
+ context?: string
26
30
  }
27
31
 
28
32
  export interface PendingApproval {
@@ -486,6 +490,7 @@ function createPikkuStreamingAdapter(
486
490
  resourceId: opts.resourceId,
487
491
  model: opts.model,
488
492
  temperature: opts.temperature,
493
+ ...(opts.context ? { context: opts.context } : {}),
489
494
  }),
490
495
  signal: abortSignal,
491
496
  credentials: opts.credentials,
@@ -948,6 +953,7 @@ function createPikkuNonStreamingAdapter(
948
953
  resourceId: opts.resourceId,
949
954
  model: opts.model,
950
955
  temperature: opts.temperature,
956
+ ...(opts.context ? { context: opts.context } : {}),
951
957
  }),
952
958
  signal: abortSignal,
953
959
  credentials: opts.credentials,