@agents24/chat-react 0.5.36 → 0.5.38

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.
Files changed (42) hide show
  1. package/README.md +31 -1
  2. package/compatibility.json +5 -5
  3. package/dist/{chunk-INO5DSSJ.js → chunk-DWDHK4FZ.js} +3 -4
  4. package/dist/chunk-DWDHK4FZ.js.map +1 -0
  5. package/dist/generated/delegation-types.d.ts +3 -2
  6. package/dist/generated/run-usage.d.ts +11 -0
  7. package/dist/registry/agents24-chat.json +13 -13
  8. package/dist/runtime/index.cjs +25 -9
  9. package/dist/runtime/index.cjs.map +1 -1
  10. package/dist/runtime/index.js +24 -7
  11. package/dist/runtime/index.js.map +1 -1
  12. package/dist/runtime/input-contract.d.ts +1 -1
  13. package/dist/runtime/use-agent-chat-hitl.d.ts +2 -2
  14. package/dist/runtime/use-agent-chat-runtime.d.ts +4 -6
  15. package/dist/scaffold/app.d.ts +2 -2
  16. package/dist/scaffold/components/chat/chat-composer.d.ts +2 -1
  17. package/dist/scaffold/components/chat/chat-message.d.ts +2 -2
  18. package/dist/scaffold/components/chat/message-parts.d.ts +2 -2
  19. package/dist/scaffold/index.cjs +17 -13
  20. package/dist/scaffold/index.cjs.map +1 -1
  21. package/dist/scaffold/index.js +11 -7
  22. package/dist/scaffold/index.js.map +1 -1
  23. package/dist/styles.css +7 -0
  24. package/dist/ui/agent-chat-composer.d.ts +9 -1
  25. package/dist/ui/agent-chat-message.d.ts +6 -2
  26. package/dist/ui/agent-response-timeline.d.ts +11 -3
  27. package/dist/ui/ask-user-interaction.d.ts +5 -1
  28. package/dist/ui/hitl-submission-status.d.ts +22 -0
  29. package/dist/ui/index.cjs +540 -340
  30. package/dist/ui/index.cjs.map +1 -1
  31. package/dist/ui/index.d.ts +1 -0
  32. package/dist/ui/index.js +536 -338
  33. package/dist/ui/index.js.map +1 -1
  34. package/dist/ui/use-agent-chat-composer.d.ts +28 -3
  35. package/dist/ui/use-audio-recorder.d.ts +15 -2
  36. package/package.json +3 -3
  37. package/scaffold/src/app.tsx +2 -1
  38. package/scaffold/src/components/chat/chat-composer.tsx +5 -3
  39. package/scaffold/src/components/chat/chat-message.tsx +3 -3
  40. package/scaffold/src/components/chat/chat-shell.tsx +2 -1
  41. package/scaffold/src/components/chat/message-parts.tsx +6 -6
  42. package/dist/chunk-INO5DSSJ.js.map +0 -1
@@ -1,6 +1,21 @@
1
1
  import * as React from "react";
2
2
  import type { AgentChatInputContract } from "../runtime/input-contract";
3
3
  import { type AgentChatComposerFile } from "./composer-attachments";
4
+ import { type RecordingCompletionContext } from "./use-audio-recorder";
5
+ export type AgentChatComposerSubmission = {
6
+ text: string;
7
+ files: AgentChatComposerFile[];
8
+ };
9
+ export type AgentChatComposerTextOptions = {
10
+ /** Focus after the controlled textarea has committed the new text. Defaults to false. */
11
+ focus?: boolean;
12
+ /** UTF-16 offsets into the new text, clamped to its bounds. Does not imply focus. */
13
+ selection?: {
14
+ start: number;
15
+ end: number;
16
+ direction?: "forward" | "backward" | "none";
17
+ };
18
+ };
4
19
  export type AgentChatComposerOptions = {
5
20
  inputContract?: AgentChatInputContract | null;
6
21
  recordingEnabled?: boolean;
@@ -10,6 +25,9 @@ export type AgentChatComposerOptions = {
10
25
  isCancelling?: boolean;
11
26
  forceExpanded?: boolean;
12
27
  focusKey?: string | null;
28
+ scopeKey?: string | null;
29
+ /** Replaces recorded-audio attachment creation; transport and policy remain host-owned. */
30
+ onRecordingComplete?: (file: File, context: RecordingCompletionContext) => void | Promise<void>;
13
31
  restoredDraft?: {
14
32
  revision: number;
15
33
  text: string;
@@ -21,12 +39,12 @@ export type AgentChatComposerOptions = {
21
39
  };
22
40
  export declare function useAgentChatComposer(options: AgentChatComposerOptions): {
23
41
  text: string;
24
- setText: (value: string) => void;
42
+ setText: (value: string, interaction?: AgentChatComposerTextOptions) => boolean;
25
43
  files: AgentChatComposerFile[];
26
44
  addFiles: (selected: readonly File[]) => void;
27
45
  removeFile: (id: string) => void;
28
46
  textareaRef: React.RefObject<HTMLTextAreaElement | null>;
29
- submit: () => Promise<void>;
47
+ submit: (submission?: AgentChatComposerSubmission) => Promise<boolean>;
30
48
  canSubmit: boolean;
31
49
  isSubmitting: boolean;
32
50
  attachmentError: string | null;
@@ -35,9 +53,16 @@ export declare function useAgentChatComposer(options: AgentChatComposerOptions):
35
53
  recorder: {
36
54
  error: string | null;
37
55
  isRecording: boolean;
38
- start: () => Promise<void>;
56
+ isStarting: boolean;
57
+ isProcessing: boolean;
58
+ isCapturing: () => boolean;
59
+ start: (startOptions?: {
60
+ operationId?: string;
61
+ }) => Promise<void>;
39
62
  stop: () => void;
63
+ cancel: () => void;
40
64
  };
65
+ isProcessing: boolean;
41
66
  recordingAllowed: boolean;
42
67
  fileAccept: string;
43
68
  onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
@@ -1,10 +1,23 @@
1
+ export type RecordingCompletionContext = {
2
+ operationId: string;
3
+ signal: AbortSignal;
4
+ };
1
5
  export type AudioRecorderOptions = {
2
6
  /** Subscribe to the recorder-owned stream; consumers must not stop its tracks. */
3
7
  onStreamChange?: (stream: MediaStream | null) => void;
8
+ /** Invalidates capture and completion work independently of focus. */
9
+ scopeKey?: string | null;
10
+ acceptedMimeTypes?: readonly string[];
4
11
  };
5
- export declare function useAudioRecorder(onRecorded: (file: File) => void, options?: AudioRecorderOptions): {
12
+ export declare function useAudioRecorder(onRecorded: (file: File, context: RecordingCompletionContext) => void | Promise<void>, options?: AudioRecorderOptions): {
6
13
  error: string | null;
7
14
  isRecording: boolean;
8
- start: () => Promise<void>;
15
+ isStarting: boolean;
16
+ isProcessing: boolean;
17
+ isCapturing: () => boolean;
18
+ start: (startOptions?: {
19
+ operationId?: string;
20
+ }) => Promise<void>;
9
21
  stop: () => void;
22
+ cancel: () => void;
10
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents24/chat-react",
3
- "version": "0.5.36",
3
+ "version": "0.5.38",
4
4
  "description": "Accessible web UI and complete default layout for Agents24 chat applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,8 +64,8 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@untitledui/file-icons": "0.0.9",
67
- "@agents24/client": "0.2.18",
68
- "@agents24/react": "0.2.23",
67
+ "@agents24/client": "0.2.20",
68
+ "@agents24/react": "0.2.25",
69
69
  "@fontsource-variable/inter": "^5.3.0",
70
70
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
71
71
  "@fontsource-variable/merriweather": "^5.3.0",
@@ -10,6 +10,7 @@ import type { HitlActionState } from "@agents24/chat-react/ui"
10
10
  import {
11
11
  Agents24Provider,
12
12
  type ChatHitlPart,
13
+ type HitlSubmissionOptions,
13
14
  type ChatMessage,
14
15
  type ChatPart,
15
16
  } from "@agents24/react"
@@ -51,7 +52,7 @@ export type Agents24ChatTimelineSlotProps = {
51
52
  parts: readonly ChatPart[]
52
53
  isLoading: boolean
53
54
  getHitlActionState: (part: ChatHitlPart) => HitlActionState | undefined
54
- onHitlAction: (part: ChatHitlPart, response: ChatHitlResponse) => void
55
+ onHitlAction: (part: ChatHitlPart, response: ChatHitlResponse, options?: HitlSubmissionOptions) => void
55
56
  }
56
57
 
57
58
  function RuntimeChat({
@@ -6,7 +6,7 @@ import {
6
6
  useAgentChatComposer,
7
7
  type AgentChatComposerFile,
8
8
  } from "@agents24/chat-react/ui"
9
- import { type AgentChatInputContract } from "@agents24/chat-react/runtime"
9
+ import { attachmentFileAccept, type AgentChatInputContract } from "@agents24/chat-react/runtime"
10
10
  import type { ContextWindow } from "@agents24/client/protocol"
11
11
 
12
12
  import { Button } from "../ui/button"
@@ -25,6 +25,7 @@ export type Agents24ChatComposerProps = {
25
25
  inputContract: AgentChatInputContract
26
26
  disabled?: boolean
27
27
  contextWindow?: ContextWindow | null
28
+ scopeKey?: string | null
28
29
  focusKey?: string | null
29
30
  forceExpanded?: boolean
30
31
  isCancelling?: boolean
@@ -48,6 +49,7 @@ export function ChatComposer({
48
49
  disabled,
49
50
  contextWindow,
50
51
  focusKey,
52
+ scopeKey,
51
53
  forceExpanded = false,
52
54
  isCancelling = false,
53
55
  isRunning,
@@ -66,7 +68,7 @@ export function ChatComposer({
66
68
  const { text, setText, files, removeFile, addFiles, textareaRef, submit,
67
69
  attachmentError, attachmentMimeTypes, allowAttachments, recordingAllowed,
68
70
  recorder, isExpanded, validationError, action, canSubmit } = useAgentChatComposer({
69
- inputContract, disabled, focusKey, forceExpanded, isCancelling, isRunning,
71
+ inputContract, disabled, scopeKey, focusKey, forceExpanded, isCancelling, isRunning,
70
72
  onSubmit, restoredDraft, onRecordingStreamChange,
71
73
  })
72
74
 
@@ -162,7 +164,7 @@ export function ChatComposer({
162
164
  className="sr-only"
163
165
  type="file"
164
166
  multiple
165
- accept={attachmentMimeTypes.join(",")}
167
+ accept={attachmentFileAccept(attachmentMimeTypes)}
166
168
  onChange={(event) => {
167
169
  addFiles(Array.from(event.target.files ?? []))
168
170
  event.currentTarget.value = ""
@@ -1,4 +1,4 @@
1
- import type { ChatHitlPart, ChatMessage as ChatMessageModel } from "@agents24/react"
1
+ import type { ChatHitlPart, HitlSubmissionOptions, ChatMessage as ChatMessageModel } from "@agents24/react"
2
2
  import type { FeedbackReason, HitlResponse } from "@agents24/client"
3
3
  import type { AgentChatRuntimeHitlActionState } from "@agents24/chat-react/runtime"
4
4
  import { AgentChatDefaultActions, AgentChatUserMessageActionRow, ChatAttachmentRows, useCompletionActionEntrance, type ChatAttachmentContentResolver } from "@agents24/chat-react/ui"
@@ -14,7 +14,7 @@ type Props = {
14
14
  message: ChatMessageModel
15
15
  getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined
16
16
  isLoading: boolean
17
- onHitlAction: (part: ChatHitlPart, response: HitlResponse) => Promise<void>
17
+ onHitlAction: (part: ChatHitlPart, response: HitlResponse, options?: HitlSubmissionOptions) => Promise<void>
18
18
  onRegenerate: () => Promise<void>
19
19
  allowFeedback: boolean
20
20
  onSetFeedback: (
@@ -71,7 +71,7 @@ export function ChatMessage({
71
71
  </AgentChatUserMessageActionRow>
72
72
  )}
73
73
  {showCompletionActions && (
74
- <MessageFooter className="gap-1 opacity-70 transition-opacity group-hover/message:opacity-100">
74
+ <MessageFooter className="a24-chat-message-action-row--assistant gap-1">
75
75
  <AgentChatDefaultActions
76
76
  animateCompletion={animateCompletionActions}
77
77
  handlers={{
@@ -24,7 +24,7 @@ import {
24
24
  import { SidebarInset, SidebarProvider, SidebarTrigger, useSidebar } from "../ui/sidebar"
25
25
 
26
26
  const LOADING_INPUT_CONTRACT = {
27
- version: "agents24.runtime-inputs.v2" as const,
27
+ version: "agents24.runtime-inputs.v3" as const,
28
28
  modalities: {
29
29
  text: { enabled: false, required: false },
30
30
  files: { enabled: false, required: false, allowed_mime_types: [] },
@@ -250,6 +250,7 @@ export function ChatShell({
250
250
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
251
251
  contextWindow={runtime.contextWindow}
252
252
  disabled={runtime.isBootstrapping || runtime.isChangingAgent || isUploading || hasBlockingError}
253
+ scopeKey={controller.activeThreadId}
253
254
  focusKey={controller.activeThreadId}
254
255
  forceExpanded={isEmptyState}
255
256
  isCancelling={isCancelling}
@@ -1,4 +1,4 @@
1
- import type { ChatHitlPart, ChatMessage } from "@agents24/react"
1
+ import type { ChatHitlPart, HitlSubmissionOptions, ChatMessage } from "@agents24/react"
2
2
  import type { HitlResponse } from "@agents24/client"
3
3
  import {
4
4
  AgentChatPart,
@@ -39,7 +39,7 @@ export function MessageParts({
39
39
  message: ChatMessage
40
40
  getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined
41
41
  isLoading: boolean
42
- onHitlAction: (part: ChatHitlPart, response: HitlResponse) => Promise<void>
42
+ onHitlAction: (part: ChatHitlPart, response: HitlResponse, options?: HitlSubmissionOptions) => Promise<void>
43
43
  Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
44
44
  }) {
45
45
  if (message.role === "assistant") {
@@ -48,8 +48,8 @@ export function MessageParts({
48
48
  getHitlActionState={getHitlActionState}
49
49
  isLoading={isLoading}
50
50
  message={message}
51
- onHitlAction={(part, response) => {
52
- void onHitlAction(part, response).catch(() => undefined)
51
+ onHitlAction={(part, response, options) => {
52
+ void onHitlAction(part, response, options).catch(() => undefined)
53
53
  }}
54
54
  parts={message.parts}
55
55
  />
@@ -71,8 +71,8 @@ export function MessageParts({
71
71
  getHitlActionState={getHitlActionState}
72
72
  key={part.id}
73
73
  message={message}
74
- onHitlAction={(hitlPart, response) => {
75
- void onHitlAction(hitlPart, response).catch(() => undefined)
74
+ onHitlAction={(hitlPart, response, options) => {
75
+ void onHitlAction(hitlPart, response, options).catch(() => undefined)
76
76
  }}
77
77
  part={part}
78
78
  />
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/input-contract.ts"],"sourcesContent":["import { normalizeAttachmentMimeType, SUPPORTED_ATTACHMENT_MIME_TYPES, type ClientInputFeature, type PortableUpload } from \"@agents24/client\";\nexport { attachmentFileAccept, normalizeAttachmentMimeType } from \"@agents24/client\";\n\nexport type AgentChatInputContract = ClientInputFeature;\n\nexport const SUPPORTED_INPUT_MIME_TYPES = SUPPORTED_ATTACHMENT_MIME_TYPES;\n\nexport type WorkflowInputRule = {\n key: string;\n enabled?: boolean;\n required?: boolean;\n};\n\nexport function workflowInputRule(\n inputs: readonly WorkflowInputRule[] | null | undefined,\n key: string,\n): { enabled: boolean; required: boolean } {\n const input = inputs?.find((candidate) => candidate.key === key);\n return {\n enabled: input?.enabled !== false,\n required: input?.required === true,\n };\n}\n\nexport function validateWorkflowInputSubmission(\n inputs: readonly WorkflowInputRule[] | null | undefined,\n text: string,\n attachmentMimeTypes: readonly string[],\n): string | null {\n const present = { files: false, images: false, audio: false };\n if (text.trim() && !workflowInputRule(inputs, \"text\").enabled) {\n return \"This workflow does not accept text input.\";\n }\n for (const rawMimeType of attachmentMimeTypes) {\n const mimeType = canonicalInputMimeType(rawMimeType);\n const modality = mimeType.startsWith(\"image/\") ? \"images\" : mimeType.startsWith(\"audio/\") ? \"audio\" : \"files\";\n const supported = (SUPPORTED_INPUT_MIME_TYPES[modality] as readonly string[]).includes(mimeType);\n if (!supported || !workflowInputRule(inputs, modality).enabled) {\n return `The attachment type ${mimeType || \"unknown\"} is not accepted.`;\n }\n present[modality] = true;\n }\n if (workflowInputRule(inputs, \"text\").required && !text.trim()) return \"Text input is required.\";\n for (const modality of [\"files\", \"images\", \"audio\"] as const) {\n if (workflowInputRule(inputs, modality).required && !present[modality]) {\n return `${modality[0].toUpperCase()}${modality.slice(1)} input is required.`;\n }\n }\n return null;\n}\n\nexport const canonicalInputMimeType = normalizeAttachmentMimeType;\n\nfunction uploadByteSize(data: PortableUpload[\"data\"]): number {\n if (data instanceof Uint8Array || data instanceof ArrayBuffer) return data.byteLength;\n return data.size;\n}\n\nexport function inputMimeTypes(contract: AgentChatInputContract | null | undefined): string[] {\n if (!contract) return [];\n return [\n ...contract.modalities.files.allowed_mime_types,\n ...contract.modalities.images.allowed_mime_types,\n ...contract.modalities.audio.allowed_mime_types,\n ];\n}\n\nexport function validateAgentChatSubmission(\n contract: AgentChatInputContract,\n text: string,\n files: readonly (Pick<PortableUpload, \"mediaType\" | \"data\"> & Partial<Pick<PortableUpload, \"name\">> & { byteSize?: number })[],\n): string | null {\n const { modalities, limits } = contract;\n const normalizedText = text.trim();\n if (normalizedText && !modalities.text.enabled) return \"This agent does not accept text input.\";\n if (modalities.text.required && !normalizedText) return \"Text input is required.\";\n if (files.length > limits.max_attachments_per_turn) {\n return `You can attach up to ${limits.max_attachments_per_turn} items per message.`;\n }\n let totalBytes = 0;\n const present = { files: false, images: false, audio: false };\n for (const file of files) {\n const mediaType = normalizeAttachmentMimeType(file.mediaType, file.name);\n const modality = mediaType.startsWith(\"image/\") ? \"images\" : mediaType.startsWith(\"audio/\") ? \"audio\" : \"files\";\n const rule = modalities[modality];\n if (!rule.enabled || !rule.allowed_mime_types.includes(mediaType)) {\n return `The attachment type ${mediaType || \"unknown\"} is not accepted.`;\n }\n const byteSize = file.byteSize ?? uploadByteSize(file.data);\n if (byteSize > limits.max_file_bytes) return \"An attachment exceeds the per-file size limit.\";\n totalBytes += byteSize;\n present[modality] = true;\n }\n if (totalBytes > limits.max_total_bytes) return \"Attachments exceed the total size limit.\";\n for (const modality of [\"files\", \"images\", \"audio\"] as const) {\n if (modalities[modality].required && !present[modality]) return `${modality[0].toUpperCase()}${modality.slice(1)} input is required.`;\n }\n return null;\n}\n"],"mappings":"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","names":["normalizeAttachmentMimeType"]}