@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.
- package/README.md +31 -1
- package/compatibility.json +5 -5
- package/dist/{chunk-INO5DSSJ.js → chunk-DWDHK4FZ.js} +3 -4
- package/dist/chunk-DWDHK4FZ.js.map +1 -0
- package/dist/generated/delegation-types.d.ts +3 -2
- package/dist/generated/run-usage.d.ts +11 -0
- package/dist/registry/agents24-chat.json +13 -13
- package/dist/runtime/index.cjs +25 -9
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +24 -7
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/input-contract.d.ts +1 -1
- package/dist/runtime/use-agent-chat-hitl.d.ts +2 -2
- package/dist/runtime/use-agent-chat-runtime.d.ts +4 -6
- package/dist/scaffold/app.d.ts +2 -2
- package/dist/scaffold/components/chat/chat-composer.d.ts +2 -1
- package/dist/scaffold/components/chat/chat-message.d.ts +2 -2
- package/dist/scaffold/components/chat/message-parts.d.ts +2 -2
- package/dist/scaffold/index.cjs +17 -13
- package/dist/scaffold/index.cjs.map +1 -1
- package/dist/scaffold/index.js +11 -7
- package/dist/scaffold/index.js.map +1 -1
- package/dist/styles.css +7 -0
- package/dist/ui/agent-chat-composer.d.ts +9 -1
- package/dist/ui/agent-chat-message.d.ts +6 -2
- package/dist/ui/agent-response-timeline.d.ts +11 -3
- package/dist/ui/ask-user-interaction.d.ts +5 -1
- package/dist/ui/hitl-submission-status.d.ts +22 -0
- package/dist/ui/index.cjs +540 -340
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +536 -338
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/use-agent-chat-composer.d.ts +28 -3
- package/dist/ui/use-audio-recorder.d.ts +15 -2
- package/package.json +3 -3
- package/scaffold/src/app.tsx +2 -1
- package/scaffold/src/components/chat/chat-composer.tsx +5 -3
- package/scaffold/src/components/chat/chat-message.tsx +3 -3
- package/scaffold/src/components/chat/chat-shell.tsx +2 -1
- package/scaffold/src/components/chat/message-parts.tsx +6 -6
- package/dist/chunk-INO5DSSJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@agents24/chat-react`
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-10-
|
|
3
|
+
Last Updated: 2026-10-25
|
|
4
4
|
|
|
5
5
|
Reusable React chat UI, runtime orchestration, and the canonical source-owned Agents24 chat scaffold.
|
|
6
6
|
|
|
@@ -30,6 +30,36 @@ Agent/model selector in the composer, preserves the active controller,
|
|
|
30
30
|
transcript, thread, and draft while switching, and adopts the durable thread
|
|
31
31
|
ID from the first accepted runtime event.
|
|
32
32
|
|
|
33
|
+
## Fill the default composer with a suggestion
|
|
34
|
+
|
|
35
|
+
Keep the default composer and its attachment handling using a public ref:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useRef } from "react";
|
|
39
|
+
import { AgentChatComposer, type AgentChatComposerHandle, type AgentChatComposerSubmit } from "@agents24/chat-react/ui";
|
|
40
|
+
|
|
41
|
+
function SuggestedComposer({ scopeKey, onSubmit }: {
|
|
42
|
+
scopeKey: string;
|
|
43
|
+
onSubmit: (message: AgentChatComposerSubmit) => void | Promise<void>;
|
|
44
|
+
}) {
|
|
45
|
+
const composerRef = useRef<AgentChatComposerHandle>(null);
|
|
46
|
+
const prompt = "Find a patient with [prescription details].";
|
|
47
|
+
return <>
|
|
48
|
+
<button type="button" onClick={() => composerRef.current?.setText(prompt, {
|
|
49
|
+
focus: true,
|
|
50
|
+
selection: { start: prompt.indexOf("["), end: prompt.indexOf("]") + 1 },
|
|
51
|
+
})}>Find a patient</button>
|
|
52
|
+
<AgentChatComposer ref={composerRef} scopeKey={scopeKey} onSubmit={onSubmit} />
|
|
53
|
+
</>;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`setText` preserves attached files and previews, updates validation/expansion, and never submits. Focus is optional and defaults to false; selection uses UTF-16 offsets into the new text and does not imply focus. Both apply after text commits. Disabled composers still accept text but skip focus and selection. Existing `textareaRef` remains available independently.
|
|
58
|
+
|
|
59
|
+
Every accepted update advances the draft revision, protecting suggestions and newer user edits from failed-send recovery. Use the current ref and change `scopeKey` with conversation/identity boundaries. Saved handles return false after a scope change or unmount; accepted updates return true. Scope changes invalidate operations; hosts continue to own conversation draft restoration through `restoredDraft`.
|
|
60
|
+
|
|
61
|
+
This API is pending the coordinated package release; it is not available in the already published `0.5.37`.
|
|
62
|
+
|
|
33
63
|
## Timeline customization
|
|
34
64
|
|
|
35
65
|
`@agents24/react` projects backend response blocks into canonical ordered `message.parts`. The default scaffold renders that sequence through `AgentResponseTimeline`, preserving interleaved text, tools, HITL, and resumed text.
|
package/compatibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractHash": "
|
|
2
|
+
"contractHash": "6d6ceb78b4285f987a5635b4d4d0b407732bf5e61abd1aca1369a97a7d6336e6",
|
|
3
3
|
"contractVersion": "0.3.0",
|
|
4
4
|
"corpus": "conformance/v1/corpus.json",
|
|
5
5
|
"generatedBy": "@agents24/sdk-contract",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"reactDom": ">=19 <20"
|
|
16
16
|
},
|
|
17
17
|
"@agents24/client": {
|
|
18
|
-
"contract": "
|
|
18
|
+
"contract": "6d6ceb78b4285f987a5635b4d4d0b407732bf5e61abd1aca1369a97a7d6336e6",
|
|
19
19
|
"runtime": "run-stream.v3"
|
|
20
20
|
},
|
|
21
21
|
"@agents24/node": {
|
|
22
|
-
"contract": "
|
|
22
|
+
"contract": "6d6ceb78b4285f987a5635b4d4d0b407732bf5e61abd1aca1369a97a7d6336e6",
|
|
23
23
|
"node": "^22.0.0 || ^24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"@agents24/react": {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"react": ">=19 <20"
|
|
28
28
|
},
|
|
29
29
|
"@talmudpedia/runtime-sdk": {
|
|
30
|
-
"contract": "
|
|
30
|
+
"contract": "6d6ceb78b4285f987a5635b4d4d0b407732bf5e61abd1aca1369a97a7d6336e6",
|
|
31
31
|
"protocolOwner": "@agents24/client"
|
|
32
32
|
},
|
|
33
33
|
"agents24": {
|
|
34
|
-
"contract": "
|
|
34
|
+
"contract": "6d6ceb78b4285f987a5635b4d4d0b407732bf5e61abd1aca1369a97a7d6336e6",
|
|
35
35
|
"python": ">=3.10"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
@@ -39,13 +39,12 @@ function uploadByteSize(data) {
|
|
|
39
39
|
function inputMimeTypes(contract) {
|
|
40
40
|
if (!contract) return [];
|
|
41
41
|
return [
|
|
42
|
-
...contract.modalities.
|
|
43
|
-
...contract.modalities.images.allowed_mime_types,
|
|
44
|
-
...contract.modalities.audio.allowed_mime_types
|
|
42
|
+
...["files", "images", "audio"].flatMap((key) => contract.modalities[key].enabled ? contract.modalities[key].allowed_mime_types : [])
|
|
45
43
|
];
|
|
46
44
|
}
|
|
47
45
|
function validateAgentChatSubmission(contract, text, files) {
|
|
48
46
|
const { modalities, limits } = contract;
|
|
47
|
+
if (files.length && contract.validation_issues?.length) return contract.validation_issues[0].message;
|
|
49
48
|
const normalizedText = text.trim();
|
|
50
49
|
if (normalizedText && !modalities.text.enabled) return "This agent does not accept text input.";
|
|
51
50
|
if (modalities.text.required && !normalizedText) return "Text input is required.";
|
|
@@ -83,4 +82,4 @@ export {
|
|
|
83
82
|
attachmentFileAccept,
|
|
84
83
|
normalizeAttachmentMimeType2 as normalizeAttachmentMimeType
|
|
85
84
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
85
|
+
//# sourceMappingURL=chunk-DWDHK4FZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 ...([\"files\", \"images\", \"audio\"] as const).flatMap(key => contract.modalities[key].enabled ? contract.modalities[key].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 if (files.length && contract.validation_issues?.length) return contract.validation_issues[0].message;\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":";AAAA,SAAS,6BAA6B,uCAAqF;AAC3H,SAAS,sBAAsB,+BAAAA,oCAAmC;AAI3D,IAAM,6BAA6B;AAQnC,SAAS,kBACd,QACA,KACyC;AACzC,QAAM,QAAQ,QAAQ,KAAK,CAAC,cAAc,UAAU,QAAQ,GAAG;AAC/D,SAAO;AAAA,IACL,SAAS,OAAO,YAAY;AAAA,IAC5B,UAAU,OAAO,aAAa;AAAA,EAChC;AACF;AAEO,SAAS,gCACd,QACA,MACA,qBACe;AACf,QAAM,UAAU,EAAE,OAAO,OAAO,QAAQ,OAAO,OAAO,MAAM;AAC5D,MAAI,KAAK,KAAK,KAAK,CAAC,kBAAkB,QAAQ,MAAM,EAAE,SAAS;AAC7D,WAAO;AAAA,EACT;AACA,aAAW,eAAe,qBAAqB;AAC7C,UAAM,WAAW,uBAAuB,WAAW;AACnD,UAAM,WAAW,SAAS,WAAW,QAAQ,IAAI,WAAW,SAAS,WAAW,QAAQ,IAAI,UAAU;AACtG,UAAM,YAAa,2BAA2B,QAAQ,EAAwB,SAAS,QAAQ;AAC/F,QAAI,CAAC,aAAa,CAAC,kBAAkB,QAAQ,QAAQ,EAAE,SAAS;AAC9D,aAAO,uBAAuB,YAAY,SAAS;AAAA,IACrD;AACA,YAAQ,QAAQ,IAAI;AAAA,EACtB;AACA,MAAI,kBAAkB,QAAQ,MAAM,EAAE,YAAY,CAAC,KAAK,KAAK,EAAG,QAAO;AACvE,aAAW,YAAY,CAAC,SAAS,UAAU,OAAO,GAAY;AAC5D,QAAI,kBAAkB,QAAQ,QAAQ,EAAE,YAAY,CAAC,QAAQ,QAAQ,GAAG;AACtE,aAAO,GAAG,SAAS,CAAC,EAAE,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,CAAC;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,yBAAyB;AAEtC,SAAS,eAAe,MAAsC;AAC5D,MAAI,gBAAgB,cAAc,gBAAgB,YAAa,QAAO,KAAK;AAC3E,SAAO,KAAK;AACd;AAEO,SAAS,eAAe,UAA+D;AAC5F,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,SAAO;AAAA,IACL,GAAI,CAAC,SAAS,UAAU,OAAO,EAAY,QAAQ,SAAO,SAAS,WAAW,GAAG,EAAE,UAAU,SAAS,WAAW,GAAG,EAAE,qBAAqB,CAAC,CAAC;AAAA,EAC/I;AACF;AAEO,SAAS,4BACd,UACA,MACA,OACe;AACf,QAAM,EAAE,YAAY,OAAO,IAAI;AAC/B,MAAI,MAAM,UAAU,SAAS,mBAAmB,OAAQ,QAAO,SAAS,kBAAkB,CAAC,EAAE;AAC7F,QAAM,iBAAiB,KAAK,KAAK;AACjC,MAAI,kBAAkB,CAAC,WAAW,KAAK,QAAS,QAAO;AACvD,MAAI,WAAW,KAAK,YAAY,CAAC,eAAgB,QAAO;AACxD,MAAI,MAAM,SAAS,OAAO,0BAA0B;AAClD,WAAO,wBAAwB,OAAO,wBAAwB;AAAA,EAChE;AACA,MAAI,aAAa;AACjB,QAAM,UAAU,EAAE,OAAO,OAAO,QAAQ,OAAO,OAAO,MAAM;AAC5D,aAAW,QAAQ,OAAO;AACxB,UAAM,YAAY,4BAA4B,KAAK,WAAW,KAAK,IAAI;AACvE,UAAM,WAAW,UAAU,WAAW,QAAQ,IAAI,WAAW,UAAU,WAAW,QAAQ,IAAI,UAAU;AACxG,UAAM,OAAO,WAAW,QAAQ;AAChC,QAAI,CAAC,KAAK,WAAW,CAAC,KAAK,mBAAmB,SAAS,SAAS,GAAG;AACjE,aAAO,uBAAuB,aAAa,SAAS;AAAA,IACtD;AACA,UAAM,WAAW,KAAK,YAAY,eAAe,KAAK,IAAI;AAC1D,QAAI,WAAW,OAAO,eAAgB,QAAO;AAC7C,kBAAc;AACd,YAAQ,QAAQ,IAAI;AAAA,EACtB;AACA,MAAI,aAAa,OAAO,gBAAiB,QAAO;AAChD,aAAW,YAAY,CAAC,SAAS,UAAU,OAAO,GAAY;AAC5D,QAAI,WAAW,QAAQ,EAAE,YAAY,CAAC,QAAQ,QAAQ,EAAG,QAAO,GAAG,SAAS,CAAC,EAAE,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,CAAC;AAAA,EAClH;AACA,SAAO;AACT;","names":["normalizeAttachmentMimeType"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PublicResponseBlock } from "@agents24/client/protocol";
|
|
2
|
+
import type { RunUsage } from "./run-usage.js";
|
|
2
3
|
export type DelegationStatus = "queued" | "running" | "waiting" | "paused" | "interrupt_requested" | "interrupted" | "completed" | "failed" | "cancelling" | "cancelled";
|
|
3
4
|
export interface DelegationCapabilities {
|
|
4
5
|
cancel: boolean;
|
|
@@ -12,7 +13,7 @@ export interface DelegationResult {
|
|
|
12
13
|
export interface DelegationInvocation {
|
|
13
14
|
run_id: string;
|
|
14
15
|
status: string;
|
|
15
|
-
usage:
|
|
16
|
+
usage: RunUsage;
|
|
16
17
|
}
|
|
17
18
|
export interface DelegationQuestionOption {
|
|
18
19
|
id: string;
|
|
@@ -86,7 +87,7 @@ export interface DelegationHistoryTurn {
|
|
|
86
87
|
response_blocks: Array<PublicResponseBlock>;
|
|
87
88
|
completed_at?: string | null;
|
|
88
89
|
final_output?: unknown;
|
|
89
|
-
run_usage?:
|
|
90
|
+
run_usage?: RunUsage | null;
|
|
90
91
|
exchanges?: Array<DelegationExchange>;
|
|
91
92
|
}
|
|
92
93
|
export interface DelegationHistoryPaging {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RunUsage = {
|
|
2
|
+
source?: "exact" | "estimated" | "unknown";
|
|
3
|
+
input_tokens?: number | null;
|
|
4
|
+
output_tokens?: number | null;
|
|
5
|
+
total_tokens?: number;
|
|
6
|
+
cached_input_tokens?: number | null;
|
|
7
|
+
cache_write_input_tokens?: number | null;
|
|
8
|
+
cache_write_input_tokens_by_ttl?: Record<string, number>;
|
|
9
|
+
cached_output_tokens?: number | null;
|
|
10
|
+
reasoning_tokens?: number | null;
|
|
11
|
+
};
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"title": "Agents24 Chat",
|
|
6
6
|
"description": "Complete editable Agents24 chat, inheriting your application's theme.",
|
|
7
7
|
"dependencies": [
|
|
8
|
-
"@agents24/chat-react@0.5.
|
|
9
|
-
"@agents24/client@0.2.
|
|
10
|
-
"@agents24/react@0.2.
|
|
8
|
+
"@agents24/chat-react@0.5.38",
|
|
9
|
+
"@agents24/client@0.2.20",
|
|
10
|
+
"@agents24/react@0.2.25",
|
|
11
11
|
"@shadcn/react@^0.2.1",
|
|
12
12
|
"class-variance-authority@^0.7.1",
|
|
13
13
|
"clsx@^2.1.1",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"path": "scaffold/src/app.tsx",
|
|
23
23
|
"type": "registry:file",
|
|
24
24
|
"target": "@components/agents24-chat/app.tsx",
|
|
25
|
-
"content": "import { createBrowserVisibility } from \"@agents24/client/browser\"\nimport type { AgentClient, HitlResponse } from \"@agents24/client\"\nimport {\n type AgentChatActiveThreadChangeHandler,\n type AgentChatMcpAuthorizationCompletion,\n type AgentChatRuntimeAgent,\n useAgentChatRuntime,\n} from \"@agents24/chat-react/runtime\"\nimport type { HitlActionState } from \"@agents24/chat-react/ui\"\nimport {\n Agents24Provider,\n type ChatHitlPart,\n type ChatMessage,\n type ChatPart,\n} from \"@agents24/react\"\nimport * as React from \"react\"\n\nimport type { Agents24ChatComposerProps } from \"./components/chat/chat-composer\"\n\nimport { ChatShell } from \"./components/chat/chat-shell\"\nimport { GUEST_CHAT_IDENTITY, type Agents24ChatIdentity } from \"./identity\"\n\nconst createBrowserAbortController = () => new AbortController()\nconst browserVisibility = createBrowserVisibility()\n\nexport type Agents24ChatAppProps = {\n activeThreadId?: string | null\n agent?: AgentChatRuntimeAgent\n client: AgentClient\n completeMcpAuthorization?: AgentChatMcpAuthorizationCompletion\n mcpRedirectUri?: string\n modelId?: string | null\n onActiveThreadIdChange?: AgentChatActiveThreadChangeHandler\n onModelChange?: (modelId: string) => void\n identity?: Agents24ChatIdentity\n components?: Agents24ChatScaffoldComponents\n sidebar?: React.ReactNode\n}\n\nexport type Agents24ChatComposerSlotProps = Agents24ChatComposerProps\n\nexport type Agents24ChatScaffoldComponents = {\n Composer?: React.ComponentType<Agents24ChatComposerSlotProps>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n}\n\nexport type ChatHitlResponse = HitlResponse\n\nexport type Agents24ChatTimelineSlotProps = {\n message: ChatMessage\n parts: readonly ChatPart[]\n isLoading: boolean\n getHitlActionState: (part: ChatHitlPart) => HitlActionState | undefined\n onHitlAction: (part: ChatHitlPart, response: ChatHitlResponse) => void\n}\n\nfunction RuntimeChat({\n components,\n identity = GUEST_CHAT_IDENTITY,\n ...props\n}: Agents24ChatAppProps) {\n const runtime = useAgentChatRuntime({\n ...props,\n mcpRedirectUri: props.mcpRedirectUri || (\n typeof window === \"undefined\" ? undefined : window.location.href\n ),\n })\n\n return <ChatShell components={components} identity={identity} runtime={runtime} sidebar={props.sidebar} />\n}\n\nexport function App(props: Agents24ChatAppProps) {\n const identity = props.identity || GUEST_CHAT_IDENTITY\n return (\n <Agents24Provider client={props.client} createAbortController={createBrowserAbortController} visibility={browserVisibility}>\n <RuntimeChat key={identity.state} {...props} />\n </Agents24Provider>\n )\n}\n"
|
|
25
|
+
"content": "import { createBrowserVisibility } from \"@agents24/client/browser\"\nimport type { AgentClient, HitlResponse } from \"@agents24/client\"\nimport {\n type AgentChatActiveThreadChangeHandler,\n type AgentChatMcpAuthorizationCompletion,\n type AgentChatRuntimeAgent,\n useAgentChatRuntime,\n} from \"@agents24/chat-react/runtime\"\nimport type { HitlActionState } from \"@agents24/chat-react/ui\"\nimport {\n Agents24Provider,\n type ChatHitlPart,\n type HitlSubmissionOptions,\n type ChatMessage,\n type ChatPart,\n} from \"@agents24/react\"\nimport * as React from \"react\"\n\nimport type { Agents24ChatComposerProps } from \"./components/chat/chat-composer\"\n\nimport { ChatShell } from \"./components/chat/chat-shell\"\nimport { GUEST_CHAT_IDENTITY, type Agents24ChatIdentity } from \"./identity\"\n\nconst createBrowserAbortController = () => new AbortController()\nconst browserVisibility = createBrowserVisibility()\n\nexport type Agents24ChatAppProps = {\n activeThreadId?: string | null\n agent?: AgentChatRuntimeAgent\n client: AgentClient\n completeMcpAuthorization?: AgentChatMcpAuthorizationCompletion\n mcpRedirectUri?: string\n modelId?: string | null\n onActiveThreadIdChange?: AgentChatActiveThreadChangeHandler\n onModelChange?: (modelId: string) => void\n identity?: Agents24ChatIdentity\n components?: Agents24ChatScaffoldComponents\n sidebar?: React.ReactNode\n}\n\nexport type Agents24ChatComposerSlotProps = Agents24ChatComposerProps\n\nexport type Agents24ChatScaffoldComponents = {\n Composer?: React.ComponentType<Agents24ChatComposerSlotProps>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n}\n\nexport type ChatHitlResponse = HitlResponse\n\nexport type Agents24ChatTimelineSlotProps = {\n message: ChatMessage\n parts: readonly ChatPart[]\n isLoading: boolean\n getHitlActionState: (part: ChatHitlPart) => HitlActionState | undefined\n onHitlAction: (part: ChatHitlPart, response: ChatHitlResponse, options?: HitlSubmissionOptions) => void\n}\n\nfunction RuntimeChat({\n components,\n identity = GUEST_CHAT_IDENTITY,\n ...props\n}: Agents24ChatAppProps) {\n const runtime = useAgentChatRuntime({\n ...props,\n mcpRedirectUri: props.mcpRedirectUri || (\n typeof window === \"undefined\" ? undefined : window.location.href\n ),\n })\n\n return <ChatShell components={components} identity={identity} runtime={runtime} sidebar={props.sidebar} />\n}\n\nexport function App(props: Agents24ChatAppProps) {\n const identity = props.identity || GUEST_CHAT_IDENTITY\n return (\n <Agents24Provider client={props.client} createAbortController={createBrowserAbortController} visibility={browserVisibility}>\n <RuntimeChat key={identity.state} {...props} />\n </Agents24Provider>\n )\n}\n"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"path": "scaffold/src/chat.css",
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"path": "scaffold/src/components/chat/chat-composer.tsx",
|
|
35
35
|
"type": "registry:file",
|
|
36
36
|
"target": "@components/agents24-chat/components/chat/chat-composer.tsx",
|
|
37
|
-
"content": "import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square } from \"lucide-react\"\nimport * as React from \"react\"\nimport {\n AgentChatContextStatus,\n ChatAttachmentRows,\n useAgentChatComposer,\n type AgentChatComposerFile,\n} from \"@agents24/chat-react/ui\"\nimport { type AgentChatInputContract } from \"@agents24/chat-react/runtime\"\nimport type { ContextWindow } from \"@agents24/client/protocol\"\n\nimport { Button } from \"../ui/button\"\nimport {\n DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem,\n DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select\"\nimport { Textarea } from \"../ui/textarea\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\"\nimport { cn } from \"../../lib/utils\"\n\nexport type Agents24ChatComposerProps = {\n queue?: React.ReactNode\n onRecordingStreamChange?: (stream: MediaStream | null) => void\n inputContract: AgentChatInputContract\n disabled?: boolean\n contextWindow?: ContextWindow | null\n focusKey?: string | null\n forceExpanded?: boolean\n isCancelling?: boolean\n isRunning: boolean\n agentId: string | null\n agents: Array<{ id: string; label: string }>\n modelId: string | null\n models: Array<{ id: string; label: string }>\n onModelChange: (modelId: string) => void\n onAgentChange: (agentId: string) => void\n onStop: () => void\n onSubmit: (text: string, files: AgentChatComposerFile[]) => Promise<void>\n restoredDraft?: { revision: number; text: string; files: AgentChatComposerFile[] } | null\n utilityRow?: \"inline\" | \"below\"\n}\n\nexport function ChatComposer({\n inputContract,\n queue,\n onRecordingStreamChange,\n disabled,\n contextWindow,\n focusKey,\n forceExpanded = false,\n isCancelling = false,\n isRunning,\n agentId,\n agents,\n modelId,\n models,\n onModelChange,\n onAgentChange,\n onStop,\n onSubmit,\n restoredDraft,\n utilityRow = \"inline\",\n}: Agents24ChatComposerProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const { text, setText, files, removeFile, addFiles, textareaRef, submit,\n attachmentError, attachmentMimeTypes, allowAttachments, recordingAllowed,\n recorder, isExpanded, validationError, action, canSubmit } = useAgentChatComposer({\n inputContract, disabled, focusKey, forceExpanded, isCancelling, isRunning,\n onSubmit, restoredDraft, onRecordingStreamChange,\n })\n\n const selectedAgent = agents.find((item) => item.id === agentId)\n const selectedModel = models.find((item) => item.id === modelId)\n const modelSelector = models.length > 0 ? (\n <div className=\"w-fit max-w-40\">\n <Select disabled={disabled || isRunning} onValueChange={onModelChange} value={modelId || undefined}>\n <SelectTrigger aria-label=\"Runtime model\">\n <SelectValue placeholder=\"Model\" />\n </SelectTrigger>\n <SelectContent align=\"start\">\n {models.map((model) => <SelectItem key={model.id} value={model.id}>{model.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </div>\n ) : null\n const runtimeSelector = agents.length > 1 ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n className=\"h-8 max-w-64 gap-1.5 bg-transparent px-2.5 text-xs font-normal hover:bg-muted/40 disabled:opacity-100\"\n disabled={disabled || isRunning}\n aria-label=\"Runtime configuration\"\n >\n <span className=\"truncate\">\n {[selectedAgent?.label, selectedModel?.label].filter(Boolean).join(\" · \") || \"Runtime\"}\n </span>\n <ChevronDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-64 rounded-lg\">\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className=\"rounded-lg\">\n <span>Agent</span><span className=\"ml-auto max-w-32 truncate text-muted-foreground\">{selectedAgent?.label}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"w-56 rounded-lg\">\n <DropdownMenuRadioGroup value={agentId || undefined} onValueChange={onAgentChange}>\n {agents.map((agent) => <DropdownMenuRadioItem className=\"rounded-lg\" key={agent.id} value={agent.id}>{agent.label}</DropdownMenuRadioItem>)}\n </DropdownMenuRadioGroup>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n {models.length > 0 ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className=\"rounded-lg\">\n <span>Model</span><span className=\"ml-auto max-w-32 truncate text-muted-foreground\">{selectedModel?.label}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-64 w-56 rounded-lg\">\n <DropdownMenuRadioGroup value={modelId || undefined} onValueChange={onModelChange}>\n {models.map((model) => <DropdownMenuRadioItem className=\"rounded-lg\" key={model.id} value={model.id}>{model.label}</DropdownMenuRadioItem>)}\n </DropdownMenuRadioGroup>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n ) : modelSelector\n const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0\n const showUtilityRow = utilityRow === \"below\"\n\n return (\n <div\n className=\"relative z-10 w-full pb-[max(1rem,env(safe-area-inset-bottom))]\"\n data-agents24-chat-composer=\"\"\n >\n {queue}\n {attachmentError || recorder.error ? <p className=\"mb-2 text-xs text-destructive\" role=\"alert\">{attachmentError || recorder.error}</p> : null}\n <div\n data-expanded={isExpanded || undefined}\n data-attachments={files.length > 0 || undefined}\n className={cn(\n \"agents24-composer-grid grid min-w-0 items-center gap-2 rounded-xl border bg-background shadow-sm\",\n isExpanded ? \"p-2\" : \"p-1.5\",\n )}\n >\n {allowAttachments && files.length > 0 && (\n <div className=\"agents24-composer-attachments min-w-0\">\n <ChatAttachmentRows\n layout=\"composer\"\n attachments={files}\n onRemove={(attachment) => {\n if (!attachment.id) return\n removeFile(attachment.id)\n }}\n />\n </div>\n )}\n {allowAttachments ? (\n <>\n <input\n ref={inputRef}\n className=\"sr-only\"\n type=\"file\"\n multiple\n accept={attachmentMimeTypes.join(\",\")}\n onChange={(event) => {\n addFiles(Array.from(event.target.files ?? []))\n event.currentTarget.value = \"\"\n }}\n />\n <div className=\"agents24-composer-attach flex min-w-0 items-center\">\n <Button className=\"bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground\" variant=\"ghost\" size=\"icon\" aria-label=\"Attach files\" disabled={disabled} onClick={() => inputRef.current?.click()}>\n <span aria-hidden>+</span>\n </Button>\n </div>\n </>\n ) : <span aria-hidden=\"true\" className=\"agents24-composer-attach\" />}\n <Textarea\n ref={textareaRef}\n value={text}\n rows={1}\n placeholder=\"Message the agent\"\n aria-label=\"Message\"\n disabled={disabled}\n className={cn(\n \"agents24-composer-textarea max-h-56 min-h-9 field-sizing-content resize-none overflow-y-auto border-0 bg-transparent px-2 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n isExpanded ? \"py-2\" : \"h-9 py-2\",\n )}\n onChange={(event) => {\n setText(event.target.value)\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault()\n void submit()\n }\n }}\n />\n <div className=\"agents24-composer-actions flex items-center gap-1\">\n {utilityRow === \"inline\" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}\n {utilityRow === \"inline\" ? runtimeSelector : null}\n {recordingAllowed ? (\n <Button\n variant={recorder.isRecording ? \"default\" : \"ghost\"}\n size=\"icon\"\n aria-label={recorder.isRecording ? \"Stop recording\" : \"Record audio\"}\n disabled={disabled || isRunning}\n onClick={() => { if (recorder.isRecording) recorder.stop(); else void recorder.start() }}\n >\n {recorder.isRecording ? <Square className=\"fill-current\" /> : <Mic />}\n </Button>\n ) : null}\n {action === \"stop\" ? (\n <Button size=\"icon\" aria-label={isCancelling ? \"Cancelling response\" : \"Stop response\"}\n disabled={disabled || isCancelling} onClick={onStop}>\n {isCancelling ? <LoaderCircle className=\"animate-spin\" /> : <Square className=\"fill-current\" />}\n </Button>\n ) : validationError ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <span tabIndex={0} aria-label={validationError}>\n <Button size=\"icon\" aria-label={isRunning ? \"Queue message\" : \"Send message\"} disabled onClick={() => void submit()}><ArrowUp /></Button>\n </span>\n </TooltipTrigger>\n <TooltipContent>{validationError}</TooltipContent>\n </Tooltip>\n ) : (\n <Button size=\"icon\" aria-label={isRunning ? \"Queue message\" : \"Send message\"} disabled={!canSubmit} onClick={() => void submit()}><ArrowUp /></Button>\n )}\n </div>\n </div>\n {showUtilityRow ? (\n <div\n aria-hidden={!runtimeSelector && !hasContextWindow ? \"true\" : undefined}\n className=\"mt-2 flex min-h-8 items-center justify-between gap-2 px-1\"\n data-agents24-chat-composer-utility=\"\"\n >\n <div className=\"min-w-0\">{runtimeSelector}</div>\n <AgentChatContextStatus contextWindow={contextWindow} />\n </div>\n ) : null}\n </div>\n )\n}\n"
|
|
37
|
+
"content": "import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square } from \"lucide-react\"\nimport * as React from \"react\"\nimport {\n AgentChatContextStatus,\n ChatAttachmentRows,\n useAgentChatComposer,\n type AgentChatComposerFile,\n} from \"@agents24/chat-react/ui\"\nimport { attachmentFileAccept, type AgentChatInputContract } from \"@agents24/chat-react/runtime\"\nimport type { ContextWindow } from \"@agents24/client/protocol\"\n\nimport { Button } from \"../ui/button\"\nimport {\n DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem,\n DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select\"\nimport { Textarea } from \"../ui/textarea\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\"\nimport { cn } from \"../../lib/utils\"\n\nexport type Agents24ChatComposerProps = {\n queue?: React.ReactNode\n onRecordingStreamChange?: (stream: MediaStream | null) => void\n inputContract: AgentChatInputContract\n disabled?: boolean\n contextWindow?: ContextWindow | null\n scopeKey?: string | null\n focusKey?: string | null\n forceExpanded?: boolean\n isCancelling?: boolean\n isRunning: boolean\n agentId: string | null\n agents: Array<{ id: string; label: string }>\n modelId: string | null\n models: Array<{ id: string; label: string }>\n onModelChange: (modelId: string) => void\n onAgentChange: (agentId: string) => void\n onStop: () => void\n onSubmit: (text: string, files: AgentChatComposerFile[]) => Promise<void>\n restoredDraft?: { revision: number; text: string; files: AgentChatComposerFile[] } | null\n utilityRow?: \"inline\" | \"below\"\n}\n\nexport function ChatComposer({\n inputContract,\n queue,\n onRecordingStreamChange,\n disabled,\n contextWindow,\n focusKey,\n scopeKey,\n forceExpanded = false,\n isCancelling = false,\n isRunning,\n agentId,\n agents,\n modelId,\n models,\n onModelChange,\n onAgentChange,\n onStop,\n onSubmit,\n restoredDraft,\n utilityRow = \"inline\",\n}: Agents24ChatComposerProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const { text, setText, files, removeFile, addFiles, textareaRef, submit,\n attachmentError, attachmentMimeTypes, allowAttachments, recordingAllowed,\n recorder, isExpanded, validationError, action, canSubmit } = useAgentChatComposer({\n inputContract, disabled, scopeKey, focusKey, forceExpanded, isCancelling, isRunning,\n onSubmit, restoredDraft, onRecordingStreamChange,\n })\n\n const selectedAgent = agents.find((item) => item.id === agentId)\n const selectedModel = models.find((item) => item.id === modelId)\n const modelSelector = models.length > 0 ? (\n <div className=\"w-fit max-w-40\">\n <Select disabled={disabled || isRunning} onValueChange={onModelChange} value={modelId || undefined}>\n <SelectTrigger aria-label=\"Runtime model\">\n <SelectValue placeholder=\"Model\" />\n </SelectTrigger>\n <SelectContent align=\"start\">\n {models.map((model) => <SelectItem key={model.id} value={model.id}>{model.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </div>\n ) : null\n const runtimeSelector = agents.length > 1 ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n className=\"h-8 max-w-64 gap-1.5 bg-transparent px-2.5 text-xs font-normal hover:bg-muted/40 disabled:opacity-100\"\n disabled={disabled || isRunning}\n aria-label=\"Runtime configuration\"\n >\n <span className=\"truncate\">\n {[selectedAgent?.label, selectedModel?.label].filter(Boolean).join(\" · \") || \"Runtime\"}\n </span>\n <ChevronDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-64 rounded-lg\">\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className=\"rounded-lg\">\n <span>Agent</span><span className=\"ml-auto max-w-32 truncate text-muted-foreground\">{selectedAgent?.label}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"w-56 rounded-lg\">\n <DropdownMenuRadioGroup value={agentId || undefined} onValueChange={onAgentChange}>\n {agents.map((agent) => <DropdownMenuRadioItem className=\"rounded-lg\" key={agent.id} value={agent.id}>{agent.label}</DropdownMenuRadioItem>)}\n </DropdownMenuRadioGroup>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n {models.length > 0 ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className=\"rounded-lg\">\n <span>Model</span><span className=\"ml-auto max-w-32 truncate text-muted-foreground\">{selectedModel?.label}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-64 w-56 rounded-lg\">\n <DropdownMenuRadioGroup value={modelId || undefined} onValueChange={onModelChange}>\n {models.map((model) => <DropdownMenuRadioItem className=\"rounded-lg\" key={model.id} value={model.id}>{model.label}</DropdownMenuRadioItem>)}\n </DropdownMenuRadioGroup>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n ) : modelSelector\n const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0\n const showUtilityRow = utilityRow === \"below\"\n\n return (\n <div\n className=\"relative z-10 w-full pb-[max(1rem,env(safe-area-inset-bottom))]\"\n data-agents24-chat-composer=\"\"\n >\n {queue}\n {attachmentError || recorder.error ? <p className=\"mb-2 text-xs text-destructive\" role=\"alert\">{attachmentError || recorder.error}</p> : null}\n <div\n data-expanded={isExpanded || undefined}\n data-attachments={files.length > 0 || undefined}\n className={cn(\n \"agents24-composer-grid grid min-w-0 items-center gap-2 rounded-xl border bg-background shadow-sm\",\n isExpanded ? \"p-2\" : \"p-1.5\",\n )}\n >\n {allowAttachments && files.length > 0 && (\n <div className=\"agents24-composer-attachments min-w-0\">\n <ChatAttachmentRows\n layout=\"composer\"\n attachments={files}\n onRemove={(attachment) => {\n if (!attachment.id) return\n removeFile(attachment.id)\n }}\n />\n </div>\n )}\n {allowAttachments ? (\n <>\n <input\n ref={inputRef}\n className=\"sr-only\"\n type=\"file\"\n multiple\n accept={attachmentFileAccept(attachmentMimeTypes)}\n onChange={(event) => {\n addFiles(Array.from(event.target.files ?? []))\n event.currentTarget.value = \"\"\n }}\n />\n <div className=\"agents24-composer-attach flex min-w-0 items-center\">\n <Button className=\"bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground\" variant=\"ghost\" size=\"icon\" aria-label=\"Attach files\" disabled={disabled} onClick={() => inputRef.current?.click()}>\n <span aria-hidden>+</span>\n </Button>\n </div>\n </>\n ) : <span aria-hidden=\"true\" className=\"agents24-composer-attach\" />}\n <Textarea\n ref={textareaRef}\n value={text}\n rows={1}\n placeholder=\"Message the agent\"\n aria-label=\"Message\"\n disabled={disabled}\n className={cn(\n \"agents24-composer-textarea max-h-56 min-h-9 field-sizing-content resize-none overflow-y-auto border-0 bg-transparent px-2 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n isExpanded ? \"py-2\" : \"h-9 py-2\",\n )}\n onChange={(event) => {\n setText(event.target.value)\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault()\n void submit()\n }\n }}\n />\n <div className=\"agents24-composer-actions flex items-center gap-1\">\n {utilityRow === \"inline\" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}\n {utilityRow === \"inline\" ? runtimeSelector : null}\n {recordingAllowed ? (\n <Button\n variant={recorder.isRecording ? \"default\" : \"ghost\"}\n size=\"icon\"\n aria-label={recorder.isRecording ? \"Stop recording\" : \"Record audio\"}\n disabled={disabled || isRunning}\n onClick={() => { if (recorder.isRecording) recorder.stop(); else void recorder.start() }}\n >\n {recorder.isRecording ? <Square className=\"fill-current\" /> : <Mic />}\n </Button>\n ) : null}\n {action === \"stop\" ? (\n <Button size=\"icon\" aria-label={isCancelling ? \"Cancelling response\" : \"Stop response\"}\n disabled={disabled || isCancelling} onClick={onStop}>\n {isCancelling ? <LoaderCircle className=\"animate-spin\" /> : <Square className=\"fill-current\" />}\n </Button>\n ) : validationError ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <span tabIndex={0} aria-label={validationError}>\n <Button size=\"icon\" aria-label={isRunning ? \"Queue message\" : \"Send message\"} disabled onClick={() => void submit()}><ArrowUp /></Button>\n </span>\n </TooltipTrigger>\n <TooltipContent>{validationError}</TooltipContent>\n </Tooltip>\n ) : (\n <Button size=\"icon\" aria-label={isRunning ? \"Queue message\" : \"Send message\"} disabled={!canSubmit} onClick={() => void submit()}><ArrowUp /></Button>\n )}\n </div>\n </div>\n {showUtilityRow ? (\n <div\n aria-hidden={!runtimeSelector && !hasContextWindow ? \"true\" : undefined}\n className=\"mt-2 flex min-h-8 items-center justify-between gap-2 px-1\"\n data-agents24-chat-composer-utility=\"\"\n >\n <div className=\"min-w-0\">{runtimeSelector}</div>\n <AgentChatContextStatus contextWindow={contextWindow} />\n </div>\n ) : null}\n </div>\n )\n}\n"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "scaffold/src/components/chat/chat-message.tsx",
|
|
41
41
|
"type": "registry:file",
|
|
42
42
|
"target": "@components/agents24-chat/components/chat/chat-message.tsx",
|
|
43
|
-
"content": "import type { ChatHitlPart, ChatMessage as ChatMessageModel } from \"@agents24/react\"\nimport type { FeedbackReason, HitlResponse } from \"@agents24/client\"\nimport type { AgentChatRuntimeHitlActionState } from \"@agents24/chat-react/runtime\"\nimport { AgentChatDefaultActions, AgentChatUserMessageActionRow, ChatAttachmentRows, useCompletionActionEntrance, type ChatAttachmentContentResolver } from \"@agents24/chat-react/ui\"\nimport * as React from \"react\"\n\nimport { MessageParts } from \"./message-parts\"\nimport { Bubble, BubbleContent } from \"../ui/bubble\"\nimport { Message, MessageContent, MessageFooter } from \"../ui/message\"\nimport type { Agents24ChatTimelineSlotProps } from \"../../app\"\n\ntype Props = {\n animateCompletion: boolean\n message: ChatMessageModel\n getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined\n isLoading: boolean\n onHitlAction: (part: ChatHitlPart, response: HitlResponse) => Promise<void>\n onRegenerate: () => Promise<void>\n allowFeedback: boolean\n onSetFeedback: (\n input: { rating: \"like\" | \"dislike\" | null; reason?: FeedbackReason | null; comment?: string | null },\n ) => Promise<void>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n resolveAttachmentContent?: ChatAttachmentContentResolver\n}\n\nexport function ChatMessage({\n animateCompletion,\n allowFeedback,\n getHitlActionState,\n isLoading,\n message,\n onHitlAction,\n onRegenerate,\n onSetFeedback,\n resolveAttachmentContent,\n Timeline,\n}: Props) {\n const isUser = message.role === \"user\"\n const showCompletionActions = !isUser && message.isFinal !== false && !isLoading\n const animateCompletionActions = useCompletionActionEntrance(showCompletionActions, message.runId, animateCompletion)\n return (\n <Message align={isUser ? \"end\" : \"start\"}>\n <MessageContent className={isUser ? \"gap-0\" : undefined}>\n {isUser && message.attachments?.length ? (\n <ChatAttachmentRows\n attachments={message.attachments}\n className=\"mb-2\"\n resolveContent={resolveAttachmentContent}\n />\n ) : null}\n <Bubble variant={isUser ? \"secondary\" : \"ghost\"} align={isUser ? \"end\" : \"start\"} className={isUser ? \"max-w-[min(75%,42rem)]\" : \"max-w-full overflow-visible\"}>\n <BubbleContent className={isUser ? \"px-4 py-2.5\" : \"w-full overflow-visible\"}>\n <MessageParts\n getHitlActionState={getHitlActionState}\n isLoading={isLoading}\n message={message}\n onHitlAction={onHitlAction}\n Timeline={Timeline}\n />\n </BubbleContent>\n </Bubble>\n {isUser && (\n <AgentChatUserMessageActionRow>\n <AgentChatDefaultActions\n handlers={{\n onCopy: (content) => { void navigator.clipboard.writeText(content).catch(() => undefined) },\n }}\n message={message}\n />\n </AgentChatUserMessageActionRow>\n )}\n {showCompletionActions && (\n <MessageFooter className=\"
|
|
43
|
+
"content": "import type { ChatHitlPart, HitlSubmissionOptions, ChatMessage as ChatMessageModel } from \"@agents24/react\"\nimport type { FeedbackReason, HitlResponse } from \"@agents24/client\"\nimport type { AgentChatRuntimeHitlActionState } from \"@agents24/chat-react/runtime\"\nimport { AgentChatDefaultActions, AgentChatUserMessageActionRow, ChatAttachmentRows, useCompletionActionEntrance, type ChatAttachmentContentResolver } from \"@agents24/chat-react/ui\"\nimport * as React from \"react\"\n\nimport { MessageParts } from \"./message-parts\"\nimport { Bubble, BubbleContent } from \"../ui/bubble\"\nimport { Message, MessageContent, MessageFooter } from \"../ui/message\"\nimport type { Agents24ChatTimelineSlotProps } from \"../../app\"\n\ntype Props = {\n animateCompletion: boolean\n message: ChatMessageModel\n getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined\n isLoading: boolean\n onHitlAction: (part: ChatHitlPart, response: HitlResponse, options?: HitlSubmissionOptions) => Promise<void>\n onRegenerate: () => Promise<void>\n allowFeedback: boolean\n onSetFeedback: (\n input: { rating: \"like\" | \"dislike\" | null; reason?: FeedbackReason | null; comment?: string | null },\n ) => Promise<void>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n resolveAttachmentContent?: ChatAttachmentContentResolver\n}\n\nexport function ChatMessage({\n animateCompletion,\n allowFeedback,\n getHitlActionState,\n isLoading,\n message,\n onHitlAction,\n onRegenerate,\n onSetFeedback,\n resolveAttachmentContent,\n Timeline,\n}: Props) {\n const isUser = message.role === \"user\"\n const showCompletionActions = !isUser && message.isFinal !== false && !isLoading\n const animateCompletionActions = useCompletionActionEntrance(showCompletionActions, message.runId, animateCompletion)\n return (\n <Message align={isUser ? \"end\" : \"start\"}>\n <MessageContent className={isUser ? \"gap-0\" : undefined}>\n {isUser && message.attachments?.length ? (\n <ChatAttachmentRows\n attachments={message.attachments}\n className=\"mb-2\"\n resolveContent={resolveAttachmentContent}\n />\n ) : null}\n <Bubble variant={isUser ? \"secondary\" : \"ghost\"} align={isUser ? \"end\" : \"start\"} className={isUser ? \"max-w-[min(75%,42rem)]\" : \"max-w-full overflow-visible\"}>\n <BubbleContent className={isUser ? \"px-4 py-2.5\" : \"w-full overflow-visible\"}>\n <MessageParts\n getHitlActionState={getHitlActionState}\n isLoading={isLoading}\n message={message}\n onHitlAction={onHitlAction}\n Timeline={Timeline}\n />\n </BubbleContent>\n </Bubble>\n {isUser && (\n <AgentChatUserMessageActionRow>\n <AgentChatDefaultActions\n handlers={{\n onCopy: (content) => { void navigator.clipboard.writeText(content).catch(() => undefined) },\n }}\n message={message}\n />\n </AgentChatUserMessageActionRow>\n )}\n {showCompletionActions && (\n <MessageFooter className=\"a24-chat-message-action-row--assistant gap-1\">\n <AgentChatDefaultActions\n animateCompletion={animateCompletionActions}\n handlers={{\n feedback: message.feedback,\n onCopy: (content) => { void navigator.clipboard.writeText(content).catch(() => undefined) },\n onRetry: () => onRegenerate(),\n onSetFeedback: allowFeedback ? (_message, input) => onSetFeedback(input) : undefined,\n }}\n message={message}\n />\n </MessageFooter>\n )}\n </MessageContent>\n </Message>\n )\n}\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "scaffold/src/components/chat/chat-shell.tsx",
|
|
47
47
|
"type": "registry:file",
|
|
48
48
|
"target": "@components/agents24-chat/components/chat/chat-shell.tsx",
|
|
49
|
-
"content": "import {\n agentChatRuntimeErrorMessage,\n type AgentChatRuntime,\n} from \"@agents24/chat-react/runtime\"\nimport type { Agents24ChatScaffoldComponents } from \"../../app\"\nimport type { ThreadMessage } from \"@agents24/client\"\nimport { AgentChatQueue, queuedMessageComposerFiles, useLiveCompletionRuns, type AgentChatComposerFile } from \"@agents24/chat-react/ui\"\nimport { CircleAlert, LoaderCircle, SquarePen, X } from \"lucide-react\"\nimport * as React from \"react\"\nimport type { Agents24ChatIdentity } from \"../../identity\"\n\nimport { ChatComposer } from \"./chat-composer\"\nimport { ChatMessage } from \"./chat-message\"\nimport { SubagentTasks } from \"./subagent-tasks\"\nimport { ThemeMenu } from \"./theme-menu\"\nimport { ThreadSidebar } from \"./thread-sidebar\"\nimport { TurnOutline } from \"./turn-outline\"\nimport { isScaffoldTimelineLoading } from \"./timeline-loading\"\nimport { Button } from \"../ui/button\"\nimport {\n LatestThreadScroller,\n latestThreadAnchorId,\n} from \"@agents24/chat-react\"\nimport { SidebarInset, SidebarProvider, SidebarTrigger, useSidebar } from \"../ui/sidebar\"\n\nconst LOADING_INPUT_CONTRACT = {\n version: \"agents24.runtime-inputs.v2\" as const,\n modalities: {\n text: { enabled: false, required: false },\n files: { enabled: false, required: false, allowed_mime_types: [] },\n images: { enabled: false, required: false, allowed_mime_types: [] },\n audio: { enabled: false, required: false, allowed_mime_types: [], recording_enabled: false },\n },\n limits: { max_attachments_per_turn: 0, max_file_bytes: 0, max_total_bytes: 0 },\n retention: { mode: \"thread_lifetime\" as const },\n}\n\nfunction PersistentSidebarTrigger() {\n return (\n <div className=\"pointer-events-none fixed start-3 top-0 z-[60] flex h-12 items-center\">\n <SidebarTrigger className=\"pointer-events-auto [&_svg]:size-[18px]\" />\n </div>\n )\n}\n\nfunction CollapsedHeaderActions({ onNew }: { onNew: () => void }) {\n const { isMobile, openMobile, state } = useSidebar()\n const sidebarVisible = isMobile ? openMobile : state === \"expanded\"\n if (sidebarVisible) return null\n return (\n <>\n <div className=\"h-8 w-17 shrink-0\" aria-hidden=\"true\" />\n <div className=\"pointer-events-none fixed start-12 top-0 z-[60] flex h-12 items-center\">\n <Button className=\"pointer-events-auto\" variant=\"ghost\" size=\"icon-sm\" aria-label=\"New chat\" onClick={onNew}>\n <SquarePen />\n </Button>\n </div>\n </>\n )\n}\n\nfunction OperationErrorNotice({ runtime }: { runtime: AgentChatRuntime }) {\n const error = runtime.operationError || runtime.backgroundError\n if (!error) return null\n return (\n <div className=\"pointer-events-none absolute inset-x-0 top-3 z-40 flex justify-center px-4\">\n <div className=\"pointer-events-auto flex max-w-lg items-start gap-2 rounded-lg border border-destructive/20 bg-background px-3 py-2 text-sm shadow-md\" role=\"alert\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <span className=\"min-w-0 flex-1 wrap-break-word\">\n {agentChatRuntimeErrorMessage(error) || \"The chat could not complete that action.\"}\n </span>\n <Button\n aria-label=\"Dismiss error\"\n onClick={runtime.operationError ? runtime.clearOperationError : runtime.controller.clearBackgroundError}\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n <X />\n </Button>\n </div>\n </div>\n )\n}\n\nexport function ChatShell({\n components,\n identity,\n runtime,\n sidebar,\n}: {\n components?: Agents24ChatScaffoldComponents\n identity: Agents24ChatIdentity\n runtime: AgentChatRuntime\n sidebar?: React.ReactNode\n}) {\n const Composer = components?.Composer || ChatComposer\n const { controller } = runtime\n const isUploading = controller.runState === \"uploading\"\n const isRunning =\n controller.runState === \"streaming\"\n || controller.runState === \"reconnecting\"\n || controller.runState === \"paused\"\n || controller.runState === \"cancelling\"\n const isCancelling = controller.runState === \"cancelling\"\n const wasLiveRun = useLiveCompletionRuns(controller.activeRunId, isRunning)\n const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title\n const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)\n const isEmptyState = controller.activeThreadId === null && controller.messages.length === 0 && !hasBlockingError\n const latestAnchorId = latestThreadAnchorId(controller.messages)\n const composerOverlayRef = React.useRef<HTMLDivElement>(null)\n const [composerOverlayHeight, setComposerOverlayHeight] = React.useState(192)\n const [restoredDraft, setRestoredDraft] = React.useState<{ revision: number; text: string; files: AgentChatComposerFile[] } | null>(null)\n const draftRevisionRef = React.useRef(0)\n const retractForEdit = React.useCallback(async (message: ThreadMessage) => {\n const draft = await controller.retractPendingMessage(message)\n setRestoredDraft({ revision: ++draftRevisionRef.current, text: draft.text,\n files: queuedMessageComposerFiles(draft),\n })\n }, [controller])\n\n React.useLayoutEffect(() => {\n const overlay = composerOverlayRef.current\n if (!overlay || isEmptyState) return\n\n const updateHeight = () => {\n setComposerOverlayHeight(Math.ceil(overlay.getBoundingClientRect().height))\n }\n\n updateHeight()\n const resizeObserver = new ResizeObserver(updateHeight)\n resizeObserver.observe(overlay)\n return () => resizeObserver.disconnect()\n }, [isEmptyState])\n\n return (\n <SidebarProvider\n className=\"h-svh min-h-0 overflow-hidden\"\n data-agents24-chat=\"\"\n data-agents24-chat-layout=\"\"\n >\n <PersistentSidebarTrigger />\n {sidebar || <ThreadSidebar\n activeThreadId={controller.activeThreadId}\n hasMore={controller.hasMoreThreads}\n identity={identity}\n isLoading={controller.isLoadingThreads}\n isLoadingMore={controller.isLoadingMoreThreads}\n threads={controller.threads}\n onDelete={runtime.deleteThread}\n onLoadMore={controller.loadMoreThreads}\n onNew={runtime.newThread}\n onOpen={runtime.openThread}\n />}\n <SidebarInset className=\"h-svh min-h-0 overflow-hidden bg-background\">\n <header\n className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border/50 px-3 sm:px-4\"\n data-agents24-chat-header=\"\"\n >\n <CollapsedHeaderActions onNew={runtime.newThread} />\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"truncate text-sm font-medium\">{String(activeTitle || \"New chat\")}</h1>\n </div>\n <ThemeMenu />\n </header>\n\n <section\n aria-label=\"Conversation\"\n className=\"relative flex min-h-0 flex-1 flex-col overflow-hidden\"\n data-agents24-chat-transcript=\"\"\n >\n <OperationErrorNotice runtime={runtime} />\n <SubagentTasks threadId={controller.activeThreadId} />\n <LatestThreadScroller.Provider autoScroll defaultScrollPosition=\"last-anchor\" scrollPreviousItemPeek={64}>\n {!isEmptyState && (\n <LatestThreadScroller.Root className=\"relative min-h-0 flex-1\">\n <LatestThreadScroller.Viewport preserveScrollOnPrepend>\n <LatestThreadScroller.Content\n className=\"mx-auto w-full max-w-3xl gap-3 px-4 pt-8 pb-0 sm:px-6 sm:pt-12\"\n style={{ paddingBottom: composerOverlayHeight + 72 }}\n >\n {controller.hasOlderTurns && (\n <Button className=\"self-center\" variant=\"ghost\" size=\"sm\" disabled={controller.isLoadingOlder} onClick={() => { void controller.loadOlder().catch(() => undefined) }}>\n {controller.isLoadingOlder && <LoaderCircle className=\"animate-spin\" />} Load earlier messages\n </Button>\n )}\n {runtime.isBootstrapping && (\n <div aria-label=\"Loading conversation\" className=\"flex flex-1 items-center justify-center py-24 text-muted-foreground\" role=\"status\">\n <LoaderCircle className=\"size-4 animate-spin\" />\n </div>\n )}\n {hasBlockingError && (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 py-24 text-center\">\n <CircleAlert className=\"size-5 text-destructive\" />\n <div><h2 className=\"font-medium\">Couldn’t connect</h2><p className=\"mt-1 max-w-md text-sm text-muted-foreground\">{runtime.errorMessage}</p></div>\n <Button variant=\"outline\" onClick={() => void runtime.retryBootstrap()}>Try again</Button>\n </div>\n )}\n {controller.messages.map((message) => (\n <LatestThreadScroller.Item\n key={message.id}\n messageId={message.id}\n scrollAnchor={isRunning && message.id === latestAnchorId}\n className=\"overflow-visible [content-visibility:visible]\"\n >\n <ChatMessage\n animateCompletion={wasLiveRun(message.runId)}\n allowFeedback={runtime.allowFeedback}\n getHitlActionState={runtime.getHitlActionState}\n isLoading={isScaffoldTimelineLoading({\n activeRunId: controller.activeRunId,\n message,\n runState: controller.runState,\n streamingMessageId: controller.streamingMessageId,\n })}\n message={message}\n onHitlAction={runtime.onHitlAction}\n onRegenerate={() => controller.regenerate(message)}\n onSetFeedback={(input) => controller.setFeedback(message, input)}\n resolveAttachmentContent={runtime.resolveAttachmentContent}\n Timeline={components?.Timeline}\n />\n </LatestThreadScroller.Item>\n ))}\n </LatestThreadScroller.Content>\n </LatestThreadScroller.Viewport>\n <TurnOutline messages={controller.messages} />\n </LatestThreadScroller.Root>\n )}\n\n <div\n ref={composerOverlayRef}\n className={isEmptyState\n ? \"pointer-events-none absolute inset-0 z-30 flex flex-col justify-center overflow-visible pb-20\"\n : \"pointer-events-none absolute inset-x-0 bottom-0 z-30 overflow-visible pt-10\"}\n style={isEmptyState ? undefined : {\n background: \"linear-gradient(to bottom, transparent 0%, color-mix(in oklch, var(--background) 85%, transparent) 45%, var(--background) 65%, var(--background) 100%)\",\n }}\n >\n {isEmptyState ? <div className=\"mx-auto mb-8 w-full max-w-3xl px-4 text-center sm:px-6\">\n <h2 className=\"text-3xl font-semibold tracking-tight sm:text-4xl\">Ready when you are.</h2>\n </div> : <div className=\"pointer-events-auto absolute inset-x-0 top-0 flex -translate-y-full justify-center pb-3\">\n <LatestThreadScroller.Button className=\"!relative !inset-auto !translate-x-0 rtl:!translate-x-0\" />\n </div>}\n <div className=\"pointer-events-auto mx-auto w-full max-w-3xl px-4 sm:px-6\">\n <Composer\n queue={<AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}\n onRemove={controller.removePendingMessage}\n onEdit={retractForEdit} onResume={controller.resumeQueue} />}\n restoredDraft={restoredDraft}\n inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}\n contextWindow={runtime.contextWindow}\n disabled={runtime.isBootstrapping || runtime.isChangingAgent || isUploading || hasBlockingError}\n focusKey={controller.activeThreadId}\n forceExpanded={isEmptyState}\n isCancelling={isCancelling}\n isRunning={isRunning}\n agentId={runtime.agentId}\n agents={runtime.agents}\n onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}\n onStop={() => { void controller.cancelRun().catch(() => undefined) }}\n modelId={runtime.modelId}\n models={runtime.models}\n onModelChange={runtime.changeModel}\n onSubmit={(text, files) => runtime.submit({\n text,\n files: files.filter((file) => file.source).map((file) => ({\n data: file.source!,\n mediaType: file.mediaType,\n name: file.filename,\n })),\n attachmentIds: files.flatMap((file) => file.persistedAttachmentId ? [file.persistedAttachmentId] : []),\n attachments: files,\n })}\n utilityRow={isEmptyState ? \"inline\" : \"below\"}\n />\n </div>\n </div>\n </LatestThreadScroller.Provider>\n </section>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n"
|
|
49
|
+
"content": "import {\n agentChatRuntimeErrorMessage,\n type AgentChatRuntime,\n} from \"@agents24/chat-react/runtime\"\nimport type { Agents24ChatScaffoldComponents } from \"../../app\"\nimport type { ThreadMessage } from \"@agents24/client\"\nimport { AgentChatQueue, queuedMessageComposerFiles, useLiveCompletionRuns, type AgentChatComposerFile } from \"@agents24/chat-react/ui\"\nimport { CircleAlert, LoaderCircle, SquarePen, X } from \"lucide-react\"\nimport * as React from \"react\"\nimport type { Agents24ChatIdentity } from \"../../identity\"\n\nimport { ChatComposer } from \"./chat-composer\"\nimport { ChatMessage } from \"./chat-message\"\nimport { SubagentTasks } from \"./subagent-tasks\"\nimport { ThemeMenu } from \"./theme-menu\"\nimport { ThreadSidebar } from \"./thread-sidebar\"\nimport { TurnOutline } from \"./turn-outline\"\nimport { isScaffoldTimelineLoading } from \"./timeline-loading\"\nimport { Button } from \"../ui/button\"\nimport {\n LatestThreadScroller,\n latestThreadAnchorId,\n} from \"@agents24/chat-react\"\nimport { SidebarInset, SidebarProvider, SidebarTrigger, useSidebar } from \"../ui/sidebar\"\n\nconst LOADING_INPUT_CONTRACT = {\n version: \"agents24.runtime-inputs.v3\" as const,\n modalities: {\n text: { enabled: false, required: false },\n files: { enabled: false, required: false, allowed_mime_types: [] },\n images: { enabled: false, required: false, allowed_mime_types: [] },\n audio: { enabled: false, required: false, allowed_mime_types: [], recording_enabled: false },\n },\n limits: { max_attachments_per_turn: 0, max_file_bytes: 0, max_total_bytes: 0 },\n retention: { mode: \"thread_lifetime\" as const },\n}\n\nfunction PersistentSidebarTrigger() {\n return (\n <div className=\"pointer-events-none fixed start-3 top-0 z-[60] flex h-12 items-center\">\n <SidebarTrigger className=\"pointer-events-auto [&_svg]:size-[18px]\" />\n </div>\n )\n}\n\nfunction CollapsedHeaderActions({ onNew }: { onNew: () => void }) {\n const { isMobile, openMobile, state } = useSidebar()\n const sidebarVisible = isMobile ? openMobile : state === \"expanded\"\n if (sidebarVisible) return null\n return (\n <>\n <div className=\"h-8 w-17 shrink-0\" aria-hidden=\"true\" />\n <div className=\"pointer-events-none fixed start-12 top-0 z-[60] flex h-12 items-center\">\n <Button className=\"pointer-events-auto\" variant=\"ghost\" size=\"icon-sm\" aria-label=\"New chat\" onClick={onNew}>\n <SquarePen />\n </Button>\n </div>\n </>\n )\n}\n\nfunction OperationErrorNotice({ runtime }: { runtime: AgentChatRuntime }) {\n const error = runtime.operationError || runtime.backgroundError\n if (!error) return null\n return (\n <div className=\"pointer-events-none absolute inset-x-0 top-3 z-40 flex justify-center px-4\">\n <div className=\"pointer-events-auto flex max-w-lg items-start gap-2 rounded-lg border border-destructive/20 bg-background px-3 py-2 text-sm shadow-md\" role=\"alert\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <span className=\"min-w-0 flex-1 wrap-break-word\">\n {agentChatRuntimeErrorMessage(error) || \"The chat could not complete that action.\"}\n </span>\n <Button\n aria-label=\"Dismiss error\"\n onClick={runtime.operationError ? runtime.clearOperationError : runtime.controller.clearBackgroundError}\n size=\"icon-xs\"\n variant=\"ghost\"\n >\n <X />\n </Button>\n </div>\n </div>\n )\n}\n\nexport function ChatShell({\n components,\n identity,\n runtime,\n sidebar,\n}: {\n components?: Agents24ChatScaffoldComponents\n identity: Agents24ChatIdentity\n runtime: AgentChatRuntime\n sidebar?: React.ReactNode\n}) {\n const Composer = components?.Composer || ChatComposer\n const { controller } = runtime\n const isUploading = controller.runState === \"uploading\"\n const isRunning =\n controller.runState === \"streaming\"\n || controller.runState === \"reconnecting\"\n || controller.runState === \"paused\"\n || controller.runState === \"cancelling\"\n const isCancelling = controller.runState === \"cancelling\"\n const wasLiveRun = useLiveCompletionRuns(controller.activeRunId, isRunning)\n const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title\n const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)\n const isEmptyState = controller.activeThreadId === null && controller.messages.length === 0 && !hasBlockingError\n const latestAnchorId = latestThreadAnchorId(controller.messages)\n const composerOverlayRef = React.useRef<HTMLDivElement>(null)\n const [composerOverlayHeight, setComposerOverlayHeight] = React.useState(192)\n const [restoredDraft, setRestoredDraft] = React.useState<{ revision: number; text: string; files: AgentChatComposerFile[] } | null>(null)\n const draftRevisionRef = React.useRef(0)\n const retractForEdit = React.useCallback(async (message: ThreadMessage) => {\n const draft = await controller.retractPendingMessage(message)\n setRestoredDraft({ revision: ++draftRevisionRef.current, text: draft.text,\n files: queuedMessageComposerFiles(draft),\n })\n }, [controller])\n\n React.useLayoutEffect(() => {\n const overlay = composerOverlayRef.current\n if (!overlay || isEmptyState) return\n\n const updateHeight = () => {\n setComposerOverlayHeight(Math.ceil(overlay.getBoundingClientRect().height))\n }\n\n updateHeight()\n const resizeObserver = new ResizeObserver(updateHeight)\n resizeObserver.observe(overlay)\n return () => resizeObserver.disconnect()\n }, [isEmptyState])\n\n return (\n <SidebarProvider\n className=\"h-svh min-h-0 overflow-hidden\"\n data-agents24-chat=\"\"\n data-agents24-chat-layout=\"\"\n >\n <PersistentSidebarTrigger />\n {sidebar || <ThreadSidebar\n activeThreadId={controller.activeThreadId}\n hasMore={controller.hasMoreThreads}\n identity={identity}\n isLoading={controller.isLoadingThreads}\n isLoadingMore={controller.isLoadingMoreThreads}\n threads={controller.threads}\n onDelete={runtime.deleteThread}\n onLoadMore={controller.loadMoreThreads}\n onNew={runtime.newThread}\n onOpen={runtime.openThread}\n />}\n <SidebarInset className=\"h-svh min-h-0 overflow-hidden bg-background\">\n <header\n className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border/50 px-3 sm:px-4\"\n data-agents24-chat-header=\"\"\n >\n <CollapsedHeaderActions onNew={runtime.newThread} />\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"truncate text-sm font-medium\">{String(activeTitle || \"New chat\")}</h1>\n </div>\n <ThemeMenu />\n </header>\n\n <section\n aria-label=\"Conversation\"\n className=\"relative flex min-h-0 flex-1 flex-col overflow-hidden\"\n data-agents24-chat-transcript=\"\"\n >\n <OperationErrorNotice runtime={runtime} />\n <SubagentTasks threadId={controller.activeThreadId} />\n <LatestThreadScroller.Provider autoScroll defaultScrollPosition=\"last-anchor\" scrollPreviousItemPeek={64}>\n {!isEmptyState && (\n <LatestThreadScroller.Root className=\"relative min-h-0 flex-1\">\n <LatestThreadScroller.Viewport preserveScrollOnPrepend>\n <LatestThreadScroller.Content\n className=\"mx-auto w-full max-w-3xl gap-3 px-4 pt-8 pb-0 sm:px-6 sm:pt-12\"\n style={{ paddingBottom: composerOverlayHeight + 72 }}\n >\n {controller.hasOlderTurns && (\n <Button className=\"self-center\" variant=\"ghost\" size=\"sm\" disabled={controller.isLoadingOlder} onClick={() => { void controller.loadOlder().catch(() => undefined) }}>\n {controller.isLoadingOlder && <LoaderCircle className=\"animate-spin\" />} Load earlier messages\n </Button>\n )}\n {runtime.isBootstrapping && (\n <div aria-label=\"Loading conversation\" className=\"flex flex-1 items-center justify-center py-24 text-muted-foreground\" role=\"status\">\n <LoaderCircle className=\"size-4 animate-spin\" />\n </div>\n )}\n {hasBlockingError && (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 py-24 text-center\">\n <CircleAlert className=\"size-5 text-destructive\" />\n <div><h2 className=\"font-medium\">Couldn’t connect</h2><p className=\"mt-1 max-w-md text-sm text-muted-foreground\">{runtime.errorMessage}</p></div>\n <Button variant=\"outline\" onClick={() => void runtime.retryBootstrap()}>Try again</Button>\n </div>\n )}\n {controller.messages.map((message) => (\n <LatestThreadScroller.Item\n key={message.id}\n messageId={message.id}\n scrollAnchor={isRunning && message.id === latestAnchorId}\n className=\"overflow-visible [content-visibility:visible]\"\n >\n <ChatMessage\n animateCompletion={wasLiveRun(message.runId)}\n allowFeedback={runtime.allowFeedback}\n getHitlActionState={runtime.getHitlActionState}\n isLoading={isScaffoldTimelineLoading({\n activeRunId: controller.activeRunId,\n message,\n runState: controller.runState,\n streamingMessageId: controller.streamingMessageId,\n })}\n message={message}\n onHitlAction={runtime.onHitlAction}\n onRegenerate={() => controller.regenerate(message)}\n onSetFeedback={(input) => controller.setFeedback(message, input)}\n resolveAttachmentContent={runtime.resolveAttachmentContent}\n Timeline={components?.Timeline}\n />\n </LatestThreadScroller.Item>\n ))}\n </LatestThreadScroller.Content>\n </LatestThreadScroller.Viewport>\n <TurnOutline messages={controller.messages} />\n </LatestThreadScroller.Root>\n )}\n\n <div\n ref={composerOverlayRef}\n className={isEmptyState\n ? \"pointer-events-none absolute inset-0 z-30 flex flex-col justify-center overflow-visible pb-20\"\n : \"pointer-events-none absolute inset-x-0 bottom-0 z-30 overflow-visible pt-10\"}\n style={isEmptyState ? undefined : {\n background: \"linear-gradient(to bottom, transparent 0%, color-mix(in oklch, var(--background) 85%, transparent) 45%, var(--background) 65%, var(--background) 100%)\",\n }}\n >\n {isEmptyState ? <div className=\"mx-auto mb-8 w-full max-w-3xl px-4 text-center sm:px-6\">\n <h2 className=\"text-3xl font-semibold tracking-tight sm:text-4xl\">Ready when you are.</h2>\n </div> : <div className=\"pointer-events-auto absolute inset-x-0 top-0 flex -translate-y-full justify-center pb-3\">\n <LatestThreadScroller.Button className=\"!relative !inset-auto !translate-x-0 rtl:!translate-x-0\" />\n </div>}\n <div className=\"pointer-events-auto mx-auto w-full max-w-3xl px-4 sm:px-6\">\n <Composer\n queue={<AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}\n onRemove={controller.removePendingMessage}\n onEdit={retractForEdit} onResume={controller.resumeQueue} />}\n restoredDraft={restoredDraft}\n inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}\n contextWindow={runtime.contextWindow}\n disabled={runtime.isBootstrapping || runtime.isChangingAgent || isUploading || hasBlockingError}\n scopeKey={controller.activeThreadId}\n focusKey={controller.activeThreadId}\n forceExpanded={isEmptyState}\n isCancelling={isCancelling}\n isRunning={isRunning}\n agentId={runtime.agentId}\n agents={runtime.agents}\n onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}\n onStop={() => { void controller.cancelRun().catch(() => undefined) }}\n modelId={runtime.modelId}\n models={runtime.models}\n onModelChange={runtime.changeModel}\n onSubmit={(text, files) => runtime.submit({\n text,\n files: files.filter((file) => file.source).map((file) => ({\n data: file.source!,\n mediaType: file.mediaType,\n name: file.filename,\n })),\n attachmentIds: files.flatMap((file) => file.persistedAttachmentId ? [file.persistedAttachmentId] : []),\n attachments: files,\n })}\n utilityRow={isEmptyState ? \"inline\" : \"below\"}\n />\n </div>\n </div>\n </LatestThreadScroller.Provider>\n </section>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"path": "scaffold/src/components/chat/collapsible-user-message.tsx",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"path": "scaffold/src/components/chat/message-parts.tsx",
|
|
71
71
|
"type": "registry:file",
|
|
72
72
|
"target": "@components/agents24-chat/components/chat/message-parts.tsx",
|
|
73
|
-
"content": "import type { ChatHitlPart, ChatMessage } from \"@agents24/react\"\nimport type { HitlResponse } from \"@agents24/client\"\nimport {\n AgentChatPart,\n AgentResponseTimeline,\n} from \"@agents24/chat-react/ui\"\nimport type { AgentChatRuntimeHitlActionState } from \"@agents24/chat-react/runtime\"\nimport * as React from \"react\"\nimport type { LinkSafetyConfig } from \"streamdown\"\n\nimport { CollapsibleUserMessage } from \"./collapsible-user-message\"\nimport { ExternalLinkDialog } from \"./external-link-dialog\"\nimport type { Agents24ChatTimelineSlotProps } from \"../../app\"\n\nconst linkSafety = {\n enabled: true,\n renderModal: (props) => <ExternalLinkDialog {...props} />,\n} satisfies LinkSafetyConfig\n\nfunction ScaffoldTimeline(props: Agents24ChatTimelineSlotProps) {\n return (\n <AgentResponseTimeline\n {...props}\n responseProps={{\n className: \"text-[15px] leading-7\",\n linkSafety,\n }}\n />\n )\n}\n\nexport function MessageParts({\n message,\n getHitlActionState,\n isLoading,\n onHitlAction,\n Timeline = ScaffoldTimeline,\n}: {\n message: ChatMessage\n getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined\n isLoading: boolean\n onHitlAction: (part: ChatHitlPart, response: HitlResponse) => Promise<void>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n}) {\n if (message.role === \"assistant\") {\n return (\n <Timeline\n getHitlActionState={getHitlActionState}\n isLoading={isLoading}\n message={message}\n onHitlAction={(part, response) => {\n void onHitlAction(part, response).catch(() => undefined)\n }}\n parts={message.parts}\n />\n )\n }\n\n const text = message.parts\n .filter((part) => part.kind === \"text\")\n .map((part) => part.kind === \"text\" ? part.text : \"\")\n .join(\"\") || message.content\n const supportingParts = message.parts.filter((part) => (\n part.kind !== \"text\" && part.kind !== \"data\"\n ))\n\n return (\n <>\n {supportingParts.map((part) => (\n <AgentChatPart\n getHitlActionState={getHitlActionState}\n key={part.id}\n message={message}\n onHitlAction={(hitlPart, response) => {\n void onHitlAction(hitlPart, response).catch(() => undefined)\n }}\n part={part}\n />\n ))}\n {text ? <CollapsibleUserMessage content={text} /> : null}\n </>\n )\n}\n"
|
|
73
|
+
"content": "import type { ChatHitlPart, HitlSubmissionOptions, ChatMessage } from \"@agents24/react\"\nimport type { HitlResponse } from \"@agents24/client\"\nimport {\n AgentChatPart,\n AgentResponseTimeline,\n} from \"@agents24/chat-react/ui\"\nimport type { AgentChatRuntimeHitlActionState } from \"@agents24/chat-react/runtime\"\nimport * as React from \"react\"\nimport type { LinkSafetyConfig } from \"streamdown\"\n\nimport { CollapsibleUserMessage } from \"./collapsible-user-message\"\nimport { ExternalLinkDialog } from \"./external-link-dialog\"\nimport type { Agents24ChatTimelineSlotProps } from \"../../app\"\n\nconst linkSafety = {\n enabled: true,\n renderModal: (props) => <ExternalLinkDialog {...props} />,\n} satisfies LinkSafetyConfig\n\nfunction ScaffoldTimeline(props: Agents24ChatTimelineSlotProps) {\n return (\n <AgentResponseTimeline\n {...props}\n responseProps={{\n className: \"text-[15px] leading-7\",\n linkSafety,\n }}\n />\n )\n}\n\nexport function MessageParts({\n message,\n getHitlActionState,\n isLoading,\n onHitlAction,\n Timeline = ScaffoldTimeline,\n}: {\n message: ChatMessage\n getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined\n isLoading: boolean\n onHitlAction: (part: ChatHitlPart, response: HitlResponse, options?: HitlSubmissionOptions) => Promise<void>\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n}) {\n if (message.role === \"assistant\") {\n return (\n <Timeline\n getHitlActionState={getHitlActionState}\n isLoading={isLoading}\n message={message}\n onHitlAction={(part, response, options) => {\n void onHitlAction(part, response, options).catch(() => undefined)\n }}\n parts={message.parts}\n />\n )\n }\n\n const text = message.parts\n .filter((part) => part.kind === \"text\")\n .map((part) => part.kind === \"text\" ? part.text : \"\")\n .join(\"\") || message.content\n const supportingParts = message.parts.filter((part) => (\n part.kind !== \"text\" && part.kind !== \"data\"\n ))\n\n return (\n <>\n {supportingParts.map((part) => (\n <AgentChatPart\n getHitlActionState={getHitlActionState}\n key={part.id}\n message={message}\n onHitlAction={(hitlPart, response, options) => {\n void onHitlAction(hitlPart, response, options).catch(() => undefined)\n }}\n part={part}\n />\n ))}\n {text ? <CollapsibleUserMessage content={text} /> : null}\n </>\n )\n}\n"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"path": "scaffold/src/components/chat/subagent-tasks.tsx",
|
|
@@ -254,13 +254,13 @@
|
|
|
254
254
|
}
|
|
255
255
|
],
|
|
256
256
|
"meta": {
|
|
257
|
-
"version": "0.5.
|
|
257
|
+
"version": "0.5.38",
|
|
258
258
|
"source": "packages/agents24-chat-react/scaffold",
|
|
259
|
-
"source_sha256": "
|
|
259
|
+
"source_sha256": "411a13a78466b84a923ae81a16d1b032deb758e7f44c1df5915cfdff71df6d62",
|
|
260
260
|
"agents24": {
|
|
261
|
-
"@agents24/client": "0.2.
|
|
262
|
-
"@agents24/react": "0.2.
|
|
263
|
-
"@agents24/chat-react": "0.5.
|
|
261
|
+
"@agents24/client": "0.2.20",
|
|
262
|
+
"@agents24/react": "0.2.25",
|
|
263
|
+
"@agents24/chat-react": "0.5.38"
|
|
264
264
|
}
|
|
265
265
|
},
|
|
266
266
|
"docs": "Import the installed styles.css from your application stylesheet. Pass your configured AgentClient to Agents24ChatScaffold. Authentication and URLs remain host-owned. Installed UI is editable; review changes before reinstalling and do not use --overwrite on customized files."
|
package/dist/runtime/index.cjs
CHANGED
|
@@ -205,14 +205,27 @@ function useAgentChatHitl(options) {
|
|
|
205
205
|
React.useEffect(() => {
|
|
206
206
|
setStates({});
|
|
207
207
|
}, [options.client, options.controller.activeThreadId]);
|
|
208
|
-
const onHitlAction = React.useCallback(async (part, response) => {
|
|
208
|
+
const onHitlAction = React.useCallback(async (part, response, submission) => {
|
|
209
209
|
const captured = latest.current;
|
|
210
210
|
const { controller, client, bootstrap, selectionGeneration } = captured;
|
|
211
|
-
const key = part.interruptId || part.id;
|
|
212
|
-
if (operations.current.has(key)) return;
|
|
213
211
|
const generation = selectionGeneration.current;
|
|
214
212
|
const threadId = controller.activeThreadId;
|
|
215
|
-
const runId = controller.messages.find((message) => message.parts.
|
|
213
|
+
const runId = part.runId || controller.messages.find((message) => message.parts.some((candidate) => candidate.id === part.id))?.runId || controller.activeRunId;
|
|
214
|
+
const key = `${generation}:${threadId}:${runId}:${part.interruptId || part.id}`;
|
|
215
|
+
if (part.hitlKind !== "mcp_auth" || response.action !== "connect") {
|
|
216
|
+
captured.clearOperationError();
|
|
217
|
+
return controller.resumeHitl(part, response, submission).catch((error) => {
|
|
218
|
+
const current = latest.current;
|
|
219
|
+
if (mounted.current && current.client === client && current.selectionGeneration.current === generation && current.controller.activeThreadId === threadId) captured.onError(error);
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const previous = operations.current.get(key);
|
|
224
|
+
if (previous?.isCurrent()) return;
|
|
225
|
+
if (previous) {
|
|
226
|
+
detach(previous);
|
|
227
|
+
operations.current.delete(key);
|
|
228
|
+
}
|
|
216
229
|
const operation = {
|
|
217
230
|
abort: new AbortController(),
|
|
218
231
|
phase: part.hitlKind === "mcp_auth" && response.action === "connect" ? "connecting" : "resuming",
|
|
@@ -284,7 +297,7 @@ function useAgentChatHitl(options) {
|
|
|
284
297
|
if (!operation.isCurrent()) return;
|
|
285
298
|
operation.phase = "resuming";
|
|
286
299
|
setStates((current) => ({ ...current, [key]: { action: response.action, status: "resuming" } }));
|
|
287
|
-
await controller.resumeHitl(part, response);
|
|
300
|
+
await controller.resumeHitl(part, response, submission);
|
|
288
301
|
} catch (error) {
|
|
289
302
|
if (!operation.isCurrent()) return;
|
|
290
303
|
captured.onError(error);
|
|
@@ -308,7 +321,11 @@ function useAgentChatHitl(options) {
|
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
323
|
}, [mcp]);
|
|
311
|
-
return { onHitlAction, getHitlActionState: (part) =>
|
|
324
|
+
return { onHitlAction, getHitlActionState: (part) => {
|
|
325
|
+
const { controller, selectionGeneration } = latest.current;
|
|
326
|
+
const runId = part.runId || controller.messages.find((message) => message.parts.some((candidate) => candidate.id === part.id))?.runId || controller.activeRunId;
|
|
327
|
+
return controller.getHitlSubmissionState(part) || states[`${selectionGeneration.current}:${controller.activeThreadId}:${runId}:${part.interruptId || part.id}`];
|
|
328
|
+
} };
|
|
312
329
|
}
|
|
313
330
|
|
|
314
331
|
// src/runtime/input-contract.ts
|
|
@@ -352,13 +369,12 @@ function uploadByteSize(data) {
|
|
|
352
369
|
function inputMimeTypes(contract) {
|
|
353
370
|
if (!contract) return [];
|
|
354
371
|
return [
|
|
355
|
-
...contract.modalities.
|
|
356
|
-
...contract.modalities.images.allowed_mime_types,
|
|
357
|
-
...contract.modalities.audio.allowed_mime_types
|
|
372
|
+
...["files", "images", "audio"].flatMap((key) => contract.modalities[key].enabled ? contract.modalities[key].allowed_mime_types : [])
|
|
358
373
|
];
|
|
359
374
|
}
|
|
360
375
|
function validateAgentChatSubmission(contract, text, files) {
|
|
361
376
|
const { modalities, limits } = contract;
|
|
377
|
+
if (files.length && contract.validation_issues?.length) return contract.validation_issues[0].message;
|
|
362
378
|
const normalizedText = text.trim();
|
|
363
379
|
if (normalizedText && !modalities.text.enabled) return "This agent does not accept text input.";
|
|
364
380
|
if (modalities.text.required && !normalizedText) return "Text input is required.";
|