@agents24/chat-react 0.5.30 → 0.5.32

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 (45) hide show
  1. package/README.md +9 -4
  2. package/dist/generated/delegation-types.d.ts +129 -0
  3. package/dist/registry/agents24-chat.json +37 -19
  4. package/dist/runtime/index.cjs +52 -0
  5. package/dist/runtime/index.cjs.map +1 -1
  6. package/dist/runtime/index.d.ts +2 -0
  7. package/dist/runtime/index.js +51 -0
  8. package/dist/runtime/index.js.map +1 -1
  9. package/dist/runtime/use-delegation-tasks.d.ts +20 -0
  10. package/dist/scaffold/app.d.ts +2 -1
  11. package/dist/scaffold/components/chat/chat-composer.d.ts +4 -1
  12. package/dist/scaffold/components/chat/subagent-tasks.d.ts +3 -0
  13. package/dist/scaffold/components/theme-provider.d.ts +4 -3
  14. package/dist/scaffold/index.cjs +425 -280
  15. package/dist/scaffold/index.cjs.map +1 -1
  16. package/dist/scaffold/index.d.ts +3 -2
  17. package/dist/scaffold/index.js +416 -271
  18. package/dist/scaffold/index.js.map +1 -1
  19. package/dist/styles.css +14 -0
  20. package/dist/ui/adapters.d.ts +5 -2
  21. package/dist/ui/agent-chat-composer.d.ts +2 -1
  22. package/dist/ui/agent-chat-queue.d.ts +0 -2
  23. package/dist/ui/index.cjs +306 -260
  24. package/dist/ui/index.cjs.map +1 -1
  25. package/dist/ui/index.js +308 -263
  26. package/dist/ui/index.js.map +1 -1
  27. package/dist/ui/tool-sessions.d.ts +10 -2
  28. package/dist/ui/use-agent-chat-composer.d.ts +25 -26
  29. package/dist/ui/use-audio-recorder.d.ts +4 -2
  30. package/package.json +4 -3
  31. package/scaffold/host-styles.css +3 -0
  32. package/scaffold/manifest.json +2 -2
  33. package/scaffold/src/app.tsx +4 -1
  34. package/scaffold/src/chat.css +11 -1
  35. package/scaffold/src/components/chat/chat-composer.tsx +36 -19
  36. package/scaffold/src/components/chat/chat-shell.tsx +5 -4
  37. package/scaffold/src/components/chat/external-link-dialog.tsx +6 -5
  38. package/scaffold/src/components/chat/identity-control.tsx +0 -1
  39. package/scaffold/src/components/chat/subagent-tasks.tsx +109 -0
  40. package/scaffold/src/components/theme-provider.tsx +30 -27
  41. package/scaffold/src/components/ui/tooltip.tsx +3 -0
  42. package/scaffold/src/host.css +11 -0
  43. package/scaffold/src/index.css +1 -213
  44. package/scaffold/src/index.tsx +4 -2
  45. package/scaffold/src/theme.css +215 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@agents24/chat-react`
2
2
 
3
- Last Updated: 2026-10-06
3
+ Last Updated: 2026-10-07
4
4
 
5
5
  Reusable React chat UI, runtime orchestration, and the canonical source-owned Agents24 chat scaffold.
6
6
 
@@ -110,6 +110,11 @@ For strict Vite CSP, load the application stylesheet with an HTML `<link>` and i
110
110
 
111
111
  The package does not own credentials, routes, Fetch/SSE, token refresh, or product-specific citations, TTS, artifacts, and debug controls. React and ReactDOM `>=19 <20` are required.
112
112
 
113
+ ## Existing hosts and customization
114
+
115
+ Use `@agents24/chat-react/scaffold/host-styles.css` for Tailwind 4 utilities and scoped chat tokens without global preflight. The default scaffold stylesheet remains suitable for standalone apps. Load the stylesheet at the public layout boundary and include copied block files in host scanning.
116
+
117
+ `components.Composer` receives `Agents24ChatComposerSlotProps`, including the canonical `queue`. Reuse `useAgentChatComposer` from `/ui` for draft/submission/recording behavior. Observe the shared microphone through `onRecordingStreamChange`; the recorder owns its tracks. `themeMode="inherit"` follows the host dark ancestor. `toolSlots.renderActivityIcon` on `AgentResponseTimeline` replaces single/grouped/planning/node orbs. Queue steering is outside the supported platform scope.
113
118
  ## Editable chat in an existing app
114
119
 
115
120
  Install the complete source-owned chat block into an existing React 19, Tailwind 4 application with shadcn configured:
@@ -120,7 +125,7 @@ npx shadcn@latest add https://docs.agents24.dev/r/agents24-chat.json
120
125
 
121
126
  The block installs under your configured components directory in `agents24-chat/`. It does not replace existing host controls, create routes, or install a server bridge. Import its `styles.css` from your application stylesheet, then import `Agents24ChatScaffold` from the installed `agents24-chat/index`. For example, with the standard `src/components` directory, add `@import "../components/agents24-chat/styles.css";` to `src/app/globals.css` in Next.js, or `@import "./components/agents24-chat/styles.css";` to `src/styles.css` in Vite. Vite with strict CSP must load that application stylesheet through an HTML `<link>`, rather than a JavaScript CSS import.
122
127
 
123
- Pass your configured `AgentClient`, and keep authentication and routing in the host. The installed block defaults to `theme="inherit"`, using host semantic colors, fonts, radii, and dark mode. It does not manage host appearance. Soft/Sharp presets remain the defaults for hosted shares and generated applications; the installed block intentionally excludes preset styles.
128
+ Pass your configured `AgentClient`, and keep authentication and routing in the host. The installed block defaults to `themeMode="inherit"`, using host semantic colors, fonts, radii, and dark mode. It does not manage host appearance. Soft/Sharp presets remain the defaults for hosted shares and generated applications; the installed block intentionally excludes preset styles.
124
129
 
125
130
  The source is yours to edit. Runtime packages remain dependencies and supply shared lifecycle fixes. Review source updates before reinstalling; do not use `--overwrite` on customized files. Each registry payload records its canonical source checksum and exact Agents24 dependency versions.
126
131
 
@@ -128,6 +133,6 @@ The source is yours to edit. Runtime packages remain dependencies and supply sha
128
133
 
129
134
  `useAgentChatComposer` from `/ui` owns draft state, attachments, input validation, audio recording, submission serialization, and untouched-draft restoration. `AgentChatComposer` remains a low-level renderer; its optional `inputContract`, `recordingEnabled`, and `contextWindow` props add contract-aware controls. Without an input contract it preserves its unrestricted host-wired behavior.
130
135
 
131
- `Agents24ChatComposer` and `Agents24ChatComposerProps` from `/scaffold` expose the complete default composer, including Agent/model selectors and context status. The complete scaffold accepts `components={{ Composer: MyComposer }}` alongside its Timeline slot. A replacement receives the complete composer props and must preserve accessible validation, recording, cancellation, and queue controls. When rendering the complete composer outside a scaffold, wrap it in the exported `Agents24ChatThemeProvider` and `Agents24ChatTooltipProvider`; pass `theme="inherit"` for host-owned appearance.
136
+ `Agents24ChatComposer` and `Agents24ChatComposerProps` from `/scaffold` expose the complete default composer, including Agent/model selectors and context status. The complete scaffold accepts `components={{ Composer: MyComposer }}` alongside its Timeline slot. A replacement receives the complete composer props and must preserve accessible validation, recording, cancellation, and queue controls. When rendering the complete composer outside a scaffold, wrap it in the exported `Agents24ChatThemeProvider` and `Agents24ChatTooltipProvider`; pass `mode="inherit"` for host-owned appearance.
132
137
 
133
- For a packaged scaffold embedded in a host theme, use `theme="inherit"` and import `@agents24/chat-react/scaffold/inherit.css` from your stylesheet instead of the preset scaffold stylesheet. Authentication remains host-owned; the scaffold provides the identity footer, not a sign-in gate.
138
+ For a packaged scaffold embedded in a host theme, use `themeMode="inherit"` and import `@agents24/chat-react/scaffold/inherit.css` from your stylesheet instead of the preset scaffold stylesheet. Authentication remains host-owned; the scaffold provides the identity footer, not a sign-in gate.
@@ -0,0 +1,129 @@
1
+ import type { PublicResponseBlock } from "@agents24/client/protocol";
2
+ export type DelegationStatus = "queued" | "running" | "waiting" | "paused" | "interrupt_requested" | "interrupted" | "completed" | "failed" | "cancelling" | "cancelled";
3
+ export interface DelegationCapabilities {
4
+ cancel: boolean;
5
+ interrupt: boolean;
6
+ }
7
+ export interface DelegationResult {
8
+ text: string | null;
9
+ output: unknown;
10
+ failure_id: string | null;
11
+ }
12
+ export interface DelegationInvocation {
13
+ run_id: string;
14
+ status: string;
15
+ usage: Record<string, unknown>;
16
+ }
17
+ export interface DelegationQuestionOption {
18
+ id: string;
19
+ label: string;
20
+ }
21
+ export interface DelegationQuestion {
22
+ id: string;
23
+ question: string;
24
+ multi_select?: boolean;
25
+ options: Array<DelegationQuestionOption>;
26
+ }
27
+ export interface DelegationQuestionPresentation {
28
+ questions?: Array<DelegationQuestion>;
29
+ }
30
+ export interface DelegationQuestionRequest {
31
+ message?: string;
32
+ presentation?: DelegationQuestionPresentation;
33
+ }
34
+ export interface DelegationPendingQuestion {
35
+ interrupt_id: string;
36
+ kind: string;
37
+ can_answer: boolean;
38
+ request: DelegationQuestionRequest;
39
+ }
40
+ export interface DelegationTask {
41
+ handle: string;
42
+ name: string;
43
+ path: string;
44
+ status: DelegationStatus;
45
+ background: boolean;
46
+ depth: number;
47
+ parent_node_id: string;
48
+ ownership_generation: number;
49
+ owner_run_id: string;
50
+ root_run_id: string;
51
+ origin: Record<string, unknown>;
52
+ run_id: string | null;
53
+ thread_id: string | null;
54
+ result_run_id: string | null;
55
+ updated_at: string;
56
+ capabilities: DelegationCapabilities;
57
+ result?: DelegationResult;
58
+ invocations?: Array<DelegationInvocation>;
59
+ pending_questions?: Array<DelegationPendingQuestion>;
60
+ }
61
+ export interface DelegationTaskList {
62
+ tasks: Array<DelegationTask>;
63
+ }
64
+ export interface DelegationAction {
65
+ action: "interrupt" | "cancel";
66
+ }
67
+ export interface DelegationActionRequest {
68
+ action: "interrupt" | "cancel";
69
+ operation_id: string;
70
+ }
71
+ export interface DelegationActionReceipt {
72
+ operation_id: string;
73
+ handle: string;
74
+ status: DelegationStatus;
75
+ }
76
+ export interface DelegationHistoryTurn {
77
+ id: string;
78
+ run_id: string;
79
+ turn_index: number;
80
+ status: string;
81
+ assignment_instruction: string;
82
+ sender: "parent";
83
+ created_at: string;
84
+ user_input_text: string | null;
85
+ assistant_output_text: string | null;
86
+ response_blocks: Array<PublicResponseBlock>;
87
+ completed_at?: string | null;
88
+ final_output?: unknown;
89
+ run_usage?: Record<string, unknown>;
90
+ exchanges?: Array<DelegationExchange>;
91
+ }
92
+ export interface DelegationHistoryPaging {
93
+ has_more: boolean;
94
+ next_before_turn_index: number | null;
95
+ }
96
+ export interface DelegationHistory {
97
+ handle: string;
98
+ parent_agent_id: string;
99
+ kind: string;
100
+ turns: Array<DelegationHistoryTurn>;
101
+ paging: DelegationHistoryPaging;
102
+ }
103
+ export interface DelegationEvent {
104
+ seq: number;
105
+ kind: string;
106
+ task: DelegationTask;
107
+ }
108
+ export interface DelegationEvents {
109
+ schema_version: "agents24.delegation.events.v1";
110
+ cursor: number;
111
+ events: Array<DelegationEvent>;
112
+ snapshot: Array<DelegationTask>;
113
+ }
114
+ export interface EmbedDelegationOptions {
115
+ externalUserId: string;
116
+ integrationId: string;
117
+ externalSessionId?: string;
118
+ runtimeTarget?: "draft" | "published";
119
+ after?: number;
120
+ before?: number;
121
+ limit?: number;
122
+ chatScope?: string;
123
+ }
124
+ export interface DelegationExchange {
125
+ id: string;
126
+ sender: "parent" | "child";
127
+ text: string;
128
+ created_at: string;
129
+ }
@@ -5,14 +5,14 @@
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.30",
9
- "@agents24/client@0.2.14",
10
- "@agents24/react@0.2.18",
8
+ "@agents24/chat-react@0.5.32",
9
+ "@agents24/client@0.2.15",
10
+ "@agents24/react@0.2.19",
11
11
  "@shadcn/react@^0.2.1",
12
12
  "class-variance-authority@^0.7.1",
13
13
  "clsx@^2.1.1",
14
- "lucide-react@0.542.0",
15
- "radix-ui@1.4.3",
14
+ "lucide-react@>=0.542.0 <2.0.0",
15
+ "radix-ui@>=1.4.3 <2.0.0",
16
16
  "streamdown@^2.5.0",
17
17
  "tailwind-merge@^3.6.0",
18
18
  "tw-animate-css@^1.4.0"
@@ -22,19 +22,19 @@
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\"\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 Agents24ChatScaffoldComponents = {\n Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>\n Composer?: React.ComponentType<Agents24ChatComposerProps>\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 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"
26
26
  },
27
27
  {
28
28
  "path": "scaffold/src/chat.css",
29
29
  "type": "registry:file",
30
30
  "target": "@components/agents24-chat/chat.css",
31
- "content": "[data-agents24-theme-root] {\n background: var(--background);\n color: var(--foreground);\n font-family: var(--font-sans);\n height: 100%;\n overflow: hidden;\n width: 100%;\n}\n\n[data-agents24-theme-root],\n[data-agents24-theme-root] * {\n box-sizing: border-box;\n -ms-overflow-style: none;\n scrollbar-width: none;\n --tw-inset-ring-shadow: 0 0 #0000 !important;\n --tw-ring-shadow: 0 0 #0000 !important;\n}\n\n[data-agents24-theme-root] ::-webkit-scrollbar {\n display: none;\n}\n\n[data-agents24-theme-root] :focus-visible {\n outline: none !important;\n}\n\n[data-agents24-theme-root] .a24-message-response {\n max-width: 100%;\n min-width: 0;\n padding-inline: 6px;\n width: 100%;\n}\n\n[data-agents24-theme-root] .a24-message-response > :first-child { margin-top: 0; }\n[data-agents24-theme-root] .a24-message-response > :last-child { margin-bottom: 0; }\n[data-agents24-theme-root] .a24-message-response :is(pre, table) {\n max-width: 100%;\n overflow-x: auto;\n}\n[data-agents24-theme-root] .a24-message-response :is(p, li, blockquote, code) {\n overflow-wrap: anywhere;\n}\n/* CSP-safe structural layout. Presets may change tokens, not these relationships. */\n.agents24-composer-grid {\n grid-template-areas: \"attach textarea submit\";\n grid-template-columns: auto minmax(0, 1fr) auto;\n}\n\n.agents24-composer-grid[data-expanded] {\n grid-template-areas: \"textarea textarea textarea\" \"attach spacer submit\";\n}\n\n[data-agents24-theme-root] .agents24-composer-grid:not([data-expanded]) {\n border-radius: var(--a24-composer-single-row-radius);\n}\n\n.agents24-composer-attach { grid-area: attach; }\n.agents24-composer-textarea { grid-area: textarea; }\n.agents24-composer-actions { grid-area: submit; }\n\n.agents24-user-message-collapsed {\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 4;\n display: -webkit-box;\n overflow: hidden;\n}\n\n/* Host theme mode changes no document tokens or appearance. */\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] {\n --a24-composer-single-row-radius: var(--radius-xl, var(--radius, .75rem));\n font-family: inherit;\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] :focus-visible {\n outline: 2px solid var(--ring, currentColor) !important;\n outline-offset: 2px;\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover {\n background: var(--popover, var(--background));\n color: var(--popover-foreground, var(--foreground));\n}\n\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] [data-slot=\"button\"] {\n border-radius: var(--radius);\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover {\n border-color: var(--border);\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover > span:not(.a24-context-status__sr-only) {\n color: var(--muted-foreground);\n}\n"
31
+ "content": "[data-agents24-theme-root] {\n background: var(--background);\n color: var(--foreground);\n font-family: var(--font-sans);\n height: 100%;\n overflow: hidden;\n width: 100%;\n}\n\n[data-agents24-theme-root],\n[data-agents24-theme-root] * {\n box-sizing: border-box;\n -ms-overflow-style: none;\n scrollbar-width: none;\n --tw-inset-ring-shadow: 0 0 #0000 !important;\n --tw-ring-shadow: 0 0 #0000 !important;\n}\n\n[data-agents24-theme-root] ::-webkit-scrollbar {\n display: none;\n}\n\n[data-agents24-theme-root] :focus-visible {\n outline: none !important;\n}\n\n[data-agents24-theme-root] .a24-message-response {\n max-width: 100%;\n min-width: 0;\n padding-inline: 6px;\n width: 100%;\n}\n\n[data-agents24-theme-root] .a24-message-response > :first-child { margin-top: 0; }\n[data-agents24-theme-root] .a24-message-response > :last-child { margin-bottom: 0; }\n[data-agents24-theme-root] .a24-message-response :is(pre, table) {\n max-width: 100%;\n overflow-x: auto;\n}\n[data-agents24-theme-root] .a24-message-response :is(p, li, blockquote, code) {\n overflow-wrap: anywhere;\n}\n/* CSP-safe structural layout. Presets may change tokens, not these relationships. */\n.agents24-composer-grid {\n grid-template-areas: \"attach textarea submit\";\n grid-template-columns: auto minmax(0, 1fr) auto;\n}\n\n.agents24-composer-grid[data-expanded] {\n grid-template-areas: \"textarea textarea textarea\" \"attach spacer submit\";\n}\n\n[data-agents24-theme-root] .agents24-composer-grid:not([data-expanded]):not([data-attachments]) {\n border-radius: var(--a24-composer-single-row-radius);\n}\n\n/* Draft attachments stay within the same surface and precede its input rows. */\n.agents24-composer-grid[data-attachments] {\n grid-template-areas: \"attachments attachments attachments\" \"attach textarea submit\";\n border-radius: 1.125rem;\n}\n.agents24-composer-grid[data-attachments][data-expanded] {\n grid-template-areas: \"attachments attachments attachments\" \"textarea textarea textarea\" \"attach spacer submit\";\n}\n.agents24-composer-attachments { grid-area: attachments; }\n\n.agents24-composer-attach { grid-area: attach; }\n.agents24-composer-textarea { grid-area: textarea; }\n.agents24-composer-actions { grid-area: submit; }\n\n.agents24-user-message-collapsed {\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 4;\n display: -webkit-box;\n overflow: hidden;\n}\n\n/* Host theme mode changes no document tokens or appearance. */\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] {\n --a24-composer-single-row-radius: var(--radius-xl, var(--radius, .75rem));\n font-family: inherit;\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] :focus-visible {\n outline: 2px solid var(--ring, currentColor) !important;\n outline-offset: 2px;\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover {\n background: var(--popover, var(--background));\n color: var(--popover-foreground, var(--foreground));\n}\n\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] [data-slot=\"button\"] {\n border-radius: var(--radius);\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover {\n border-color: var(--border);\n}\n[data-agents24-theme-root][data-agents24-theme=\"inherit\"] .a24-context-status__popover > span:not(.a24-context-status__sr-only) {\n color: var(--muted-foreground);\n}\n"
32
32
  },
33
33
  {
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 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 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 textareaRef = React.useRef<HTMLTextAreaElement>(null)\n const composer = useAgentChatComposer({ inputContract, disabled, isRunning, isCancelling,\n focusKey, restoredDraft, textareaRef, onSubmit: ({ text, files }) => onSubmit(text, files) })\n const { text, files, setText, validationError, attachmentError, recorder, recordingAllowed,\n allowAttachments, action, submit } = composer\n const isExpanded = forceExpanded || text.includes(\"\\n\") || text.length > 62\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 {allowAttachments && files.length > 0 && (\n <div className=\"mb-2 min-w-0\">\n <ChatAttachmentRows\n attachments={files}\n onRemove={(attachment) => { if (attachment.id) composer.removeFile(attachment.id) }}\n />\n </div>\n )}\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 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 ? (\n <>\n <input\n ref={inputRef}\n className=\"sr-only\"\n type=\"file\"\n multiple\n accept={composer.fileAccept}\n onChange={(event) => {\n composer.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) => setText(event.target.value)}\n onKeyDown={composer.onKeyDown}\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={disabled || !composer.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 { 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"
38
38
  },
39
39
  {
40
40
  "path": "scaffold/src/components/chat/chat-message.tsx",
@@ -46,7 +46,7 @@
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 { 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 <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 <AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}\n canSteer={controller.runState === \"streaming\"}\n onSteer={controller.steerPendingMessage} onRemove={controller.removePendingMessage}\n onEdit={retractForEdit} onResume={controller.resumeQueue} />\n <Composer\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.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"
50
50
  },
51
51
  {
52
52
  "path": "scaffold/src/components/chat/collapsible-user-message.tsx",
@@ -58,13 +58,13 @@
58
58
  "path": "scaffold/src/components/chat/external-link-dialog.tsx",
59
59
  "type": "registry:file",
60
60
  "target": "@components/agents24-chat/components/chat/external-link-dialog.tsx",
61
- "content": "import type { LinkSafetyModalProps } from \"streamdown\"\nimport * as React from \"react\"\n\nimport { Button } from \"../ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"../ui/dialog\"\n\nexport function ExternalLinkDialog({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) {\n const [copied, setCopied] = React.useState(false)\n\n React.useEffect(() => {\n if (!isOpen) setCopied(false)\n }, [isOpen])\n\n const copyUrl = async () => {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n }\n\n const openUrl = () => {\n onConfirm()\n onClose()\n }\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose() }}>\n <DialogContent className=\"max-h-[calc(100dvh-2rem)] min-w-0 overflow-hidden sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>Open external link?</DialogTitle>\n <DialogDescription>You’re about to visit an external website.</DialogDescription>\n </DialogHeader>\n <div className=\"max-h-32 overflow-auto rounded-md bg-muted p-3 font-mono text-xs wrap-break-word\">\n {url}\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => void copyUrl()}>{copied ? \"Copied\" : \"Copy link\"}</Button>\n <Button onClick={openUrl}>Open link</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"
61
+ "content": "import type { LinkSafetyModalProps } from \"streamdown\"\nimport * as React from \"react\"\n\nimport { Button } from \"../ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"../ui/dialog\"\n\nexport function ExternalLinkDialog({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) {\n const [copied, setCopied] = React.useState(false)\n\n const close = () => {\n setCopied(false)\n onClose()\n }\n\n const copyUrl = async () => {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n }\n\n const openUrl = () => {\n onConfirm()\n close()\n }\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => { if (!open) close() }}>\n <DialogContent className=\"max-h-[calc(100dvh-2rem)] min-w-0 overflow-hidden sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>Open external link?</DialogTitle>\n <DialogDescription>You’re about to visit an external website.</DialogDescription>\n </DialogHeader>\n <div className=\"max-h-32 overflow-auto rounded-md bg-muted p-3 font-mono text-xs wrap-break-word\">\n {url}\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => void copyUrl()}>{copied ? \"Copied\" : \"Copy link\"}</Button>\n <Button onClick={openUrl}>Open link</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"
62
62
  },
63
63
  {
64
64
  "path": "scaffold/src/components/chat/identity-control.tsx",
65
65
  "type": "registry:file",
66
66
  "target": "@components/agents24-chat/components/chat/identity-control.tsx",
67
- "content": "import { LogIn, LogOut, UserRound } from \"lucide-react\"\n\nimport type { Agents24ChatIdentity } from \"../../identity\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\"\nimport { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from \"../ui/sidebar\"\n\nfunction identityLabel(identity: Agents24ChatIdentity): string {\n if (identity.displayName) return identity.displayName\n if (identity.state === \"authenticated\") return \"Signed in\"\n if (identity.state === \"loading\") return \"Loading identity\"\n if (identity.state === \"error\") return \"Identity unavailable\"\n if (identity.state === \"signed_out\") return \"Sign in\"\n return \"Guest\"\n}\n\nfunction IdentityAvatar({ identity }: { identity: Agents24ChatIdentity }) {\n const label = identityLabel(identity)\n if (identity.avatarUrl) {\n return <img alt=\"\" className=\"size-6 rounded-full object-cover\" src={identity.avatarUrl} />\n }\n return (\n <span className=\"flex size-6 items-center justify-center rounded-full bg-sidebar-accent text-[10px] font-semibold\" aria-hidden=\"true\">\n {identity.displayName ? identity.displayName.slice(0, 1).toUpperCase() : <UserRound className=\"size-3.5\" />}\n </span>\n )\n}\n\nexport function IdentityControl({ identity }: { identity: Agents24ChatIdentity }) {\n const label = identityLabel(identity)\n const canSignOut = identity.state === \"authenticated\" && Boolean(identity.onSignOut)\n const canSignIn = (identity.state === \"guest\" || identity.state === \"signed_out\" || identity.state === \"error\") && Boolean(identity.onSignIn)\n return (\n <SidebarMenu className=\"w-full min-w-0\">\n <SidebarMenuItem className=\"w-full min-w-0\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n aria-label={`Account: ${label}`}\n className=\"w-full min-w-0 justify-start\"\n >\n <IdentityAvatar identity={identity} />\n <span className=\"min-w-0 flex-1 overflow-hidden text-start\">\n <span className=\"block truncate text-sm font-medium\">{label}</span>\n {identity.secondaryText ? <span className=\"block truncate text-xs text-muted-foreground\">{identity.secondaryText}</span> : null}\n </span>\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" side=\"top\" sideOffset={8}>\n <DropdownMenuLabel>\n <span className=\"block truncate\">{label}</span>\n {identity.secondaryText ? <span className=\"mt-0.5 block truncate text-xs font-normal text-muted-foreground\">{identity.secondaryText}</span> : null}\n </DropdownMenuLabel>\n {(canSignIn || canSignOut) ? <DropdownMenuSeparator /> : null}\n {canSignIn ? <DropdownMenuItem onSelect={() => identity.onSignIn?.()}><LogIn />Sign in</DropdownMenuItem> : null}\n {canSignOut ? <DropdownMenuItem onSelect={() => identity.onSignOut?.()}><LogOut />Sign out</DropdownMenuItem> : null}\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n"
67
+ "content": "import { LogIn, LogOut, UserRound } from \"lucide-react\"\n\nimport type { Agents24ChatIdentity } from \"../../identity\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\"\nimport { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from \"../ui/sidebar\"\n\nfunction identityLabel(identity: Agents24ChatIdentity): string {\n if (identity.displayName) return identity.displayName\n if (identity.state === \"authenticated\") return \"Signed in\"\n if (identity.state === \"loading\") return \"Loading identity\"\n if (identity.state === \"error\") return \"Identity unavailable\"\n if (identity.state === \"signed_out\") return \"Sign in\"\n return \"Guest\"\n}\n\nfunction IdentityAvatar({ identity }: { identity: Agents24ChatIdentity }) {\n if (identity.avatarUrl) {\n return <img alt=\"\" className=\"size-6 rounded-full object-cover\" src={identity.avatarUrl} />\n }\n return (\n <span className=\"flex size-6 items-center justify-center rounded-full bg-sidebar-accent text-[10px] font-semibold\" aria-hidden=\"true\">\n {identity.displayName ? identity.displayName.slice(0, 1).toUpperCase() : <UserRound className=\"size-3.5\" />}\n </span>\n )\n}\n\nexport function IdentityControl({ identity }: { identity: Agents24ChatIdentity }) {\n const label = identityLabel(identity)\n const canSignOut = identity.state === \"authenticated\" && Boolean(identity.onSignOut)\n const canSignIn = (identity.state === \"guest\" || identity.state === \"signed_out\" || identity.state === \"error\") && Boolean(identity.onSignIn)\n return (\n <SidebarMenu className=\"w-full min-w-0\">\n <SidebarMenuItem className=\"w-full min-w-0\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n aria-label={`Account: ${label}`}\n className=\"w-full min-w-0 justify-start\"\n >\n <IdentityAvatar identity={identity} />\n <span className=\"min-w-0 flex-1 overflow-hidden text-start\">\n <span className=\"block truncate text-sm font-medium\">{label}</span>\n {identity.secondaryText ? <span className=\"block truncate text-xs text-muted-foreground\">{identity.secondaryText}</span> : null}\n </span>\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" side=\"top\" sideOffset={8}>\n <DropdownMenuLabel>\n <span className=\"block truncate\">{label}</span>\n {identity.secondaryText ? <span className=\"mt-0.5 block truncate text-xs font-normal text-muted-foreground\">{identity.secondaryText}</span> : null}\n </DropdownMenuLabel>\n {(canSignIn || canSignOut) ? <DropdownMenuSeparator /> : null}\n {canSignIn ? <DropdownMenuItem onSelect={() => identity.onSignIn?.()}><LogIn />Sign in</DropdownMenuItem> : null}\n {canSignOut ? <DropdownMenuItem onSelect={() => identity.onSignOut?.()}><LogOut />Sign out</DropdownMenuItem> : null}\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n"
68
68
  },
69
69
  {
70
70
  "path": "scaffold/src/components/chat/message-parts.tsx",
@@ -72,6 +72,12 @@
72
72
  "target": "@components/agents24-chat/components/chat/message-parts.tsx",
73
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"
74
74
  },
75
+ {
76
+ "path": "scaffold/src/components/chat/subagent-tasks.tsx",
77
+ "type": "registry:file",
78
+ "target": "@components/agents24-chat/components/chat/subagent-tasks.tsx",
79
+ "content": "import * as React from \"react\"\nimport { useAgentClient, partsFromResponseBlocks } from \"@agents24/react\"\nimport { AgentResponseTimeline } from \"@agents24/chat-react/ui\"\nimport { useDelegationTasks, type DelegationHistory, type DelegationTask } from \"@agents24/chat-react/runtime\"\nimport { Button } from \"../ui/button\"\n\nexport function SubagentTasks({ threadId }: { threadId: string | null }) {\n const client = useAgentClient()\n const source = React.useMemo(() => ({ events: (input: { threadId: string; after: number; signal: AbortSignal }) =>\n client.threads.delegations.events(input) }), [client])\n const { tasks, error, refresh } = useDelegationTasks({ threadId, source })\n const [selection, setSelection] = React.useState<{ threadId: string | null; handle: string | null }>({ threadId: null, handle: null })\n const selected = selection.threadId === threadId ? selection.handle : null\n if (!threadId || (!tasks.length && !error)) return null\n return <section aria-label=\"Subagent tasks\" className=\"mx-auto w-full max-w-3xl px-4 py-2 text-sm\">\n <details>\n <summary className=\"cursor-pointer font-medium\">Tasks ({tasks.length})</summary>\n {error ? <p role=\"alert\">Task updates are unavailable. Retrying…</p> : null}\n <ul className=\"max-h-56 overflow-y-auto\">\n {tasks.map(task => <li key={task.handle} style={{ paddingInlineStart: Math.max(0, task.depth - 1) * 12 }}>\n <button type=\"button\" className=\"flex w-full items-center justify-between gap-3 rounded-md px-2 py-2 text-start hover:bg-muted\"\n onClick={() => setSelection({ threadId, handle: selected === task.handle ? null : task.handle })} aria-expanded={selected === task.handle}>\n <span className=\"min-w-0 truncate\">{task.name}{task.background ? \" · Background\" : \"\"}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{task.status.replaceAll(\"_\", \" \")}</span>\n </button>\n </li>)}\n </ul>\n {tasks.filter(task => task.handle === selected).map(task =>\n <TaskConversation key={`${threadId}:${task.handle}`} threadId={threadId} task={task} onChanged={refresh} />)}\n </details>\n </section>\n}\n\nfunction TaskConversation({ threadId, task, onChanged }: {\n threadId: string; task: DelegationTask; onChanged: () => void\n}) {\n const client = useAgentClient()\n const [history, setHistory] = React.useState<DelegationHistory | null>(null)\n const [earlier, setEarlier] = React.useState<DelegationHistory[\"turns\"]>([])\n const historyInitialized = React.useRef(false)\n const [before, setBefore] = React.useState<number | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [busy, setBusy] = React.useState(false)\n const operation = React.useRef<{ action: \"interrupt\" | \"cancel\"; id: string } | null>(null)\n React.useEffect(() => {\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout>\n const refresh = async () => {\n try {\n const page = await client.threads.delegations.history({ threadId, handle: task.handle, signal: controller.signal })\n if (controller.signal.aborted) return\n setHistory(page)\n if (!historyInitialized.current) {\n historyInitialized.current = true\n setBefore(page.paging.next_before_turn_index)\n }\n } catch {\n if (!controller.signal.aborted) setError(\"Task history could not be loaded.\")\n } finally {\n if (!controller.signal.aborted) timer = setTimeout(refresh, 2000)\n }\n }\n void refresh()\n return () => { controller.abort(); clearTimeout(timer) }\n }, [client, threadId, task.handle])\n const loadEarlier = async () => {\n if (before === null) return\n setBusy(true)\n try {\n const page = await client.threads.delegations.history({ threadId, handle: task.handle, before })\n setEarlier(current => [...page.turns, ...current])\n setBefore(page.paging.next_before_turn_index)\n } catch { setError(\"Earlier task history could not be loaded.\") }\n finally { setBusy(false) }\n }\n const act = async (action: \"interrupt\" | \"cancel\") => {\n setBusy(true); setError(null)\n if (operation.current?.action !== action) operation.current = { action, id: crypto.randomUUID() }\n try {\n await client.threads.delegations.act({ threadId, handle: task.handle,\n action: { action, operation_id: operation.current.id } })\n operation.current = null\n onChanged()\n } catch { setError(\"The task action could not be completed. Try again.\") }\n finally { setBusy(false) }\n }\n const turns = Array.from(new Map([...earlier, ...(history?.turns || [])].map(turn => [turn.id, turn])).values())\n .sort((a, b) => a.turn_index - b.turn_index)\n return <div className=\"space-y-3 rounded-md bg-muted/30 p-3\">\n <h3 className=\"font-medium\">{task.name}</h3>\n {error ? <p role=\"alert\">{error}</p> : null}\n <div className=\"flex flex-wrap gap-2\">\n {task.capabilities.interrupt ? <Button variant=\"secondary\" size=\"sm\" disabled={busy} onClick={() => void act(\"interrupt\")}>Interrupt</Button> : null}\n {task.capabilities.cancel ? <Button variant=\"secondary\" size=\"sm\" disabled={busy} onClick={() => void act(\"cancel\")}>Cancel task</Button> : null}\n {before !== null ? <Button variant=\"ghost\" size=\"sm\" disabled={busy} onClick={() => void loadEarlier()}>Earlier exchanges</Button> : null}\n </div>\n <div className=\"max-h-[55dvh] space-y-4 overflow-y-auto break-words\">\n {turns.map(turn => <article key={turn.id} className=\"space-y-2\">\n {turn.assignment_instruction ? <p className=\"whitespace-pre-wrap text-muted-foreground\">{turn.assignment_instruction}</p> : null}\n {turn.user_input_text ? <p className=\"whitespace-pre-wrap\">{turn.user_input_text}</p> : null}\n {turn.exchanges?.map(exchange => <div key={exchange.id} className=\"whitespace-pre-wrap break-words text-sm\">\n <p className=\"text-xs font-medium\">{exchange.sender === \"parent\" ? \"Parent Agent\" : \"Subagent update\"}</p>\n {exchange.text}\n </div>)}\n <AgentResponseTimeline message={{ id: turn.id, role: \"assistant\", createdAt: turn.created_at, content: turn.assistant_output_text || \"\", parts: partsFromResponseBlocks(turn.response_blocks) }} />\n </article>)}\n </div>\n </div>\n}\n"
80
+ },
75
81
  {
76
82
  "path": "scaffold/src/components/chat/theme-menu.tsx",
77
83
  "type": "registry:file",
@@ -100,7 +106,7 @@
100
106
  "path": "scaffold/src/components/theme-provider.tsx",
101
107
  "type": "registry:file",
102
108
  "target": "@components/agents24-chat/components/theme-provider.tsx",
103
- "content": "import * as React from \"react\"\n\nexport type Agents24ChatAppearance = \"system\" | \"light\" | \"dark\"\nexport type Agents24ChatTheme = \"soft\" | \"sharp\" | \"inherit\"\n\nexport type Agents24ChatAppearanceController = {\n appearance: Agents24ChatAppearance\n setAppearance: (appearance: Agents24ChatAppearance) => void\n}\n\ntype ThemeContextValue = {\n appearance: Agents24ChatAppearance\n inherited: boolean\n portalContainer: HTMLElement | null\n setAppearance: (appearance: Agents24ChatAppearance) => void\n}\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null)\nconst storageKey = \"agents24-chat-appearance\"\n\nfunction storedAppearance(): Agents24ChatAppearance {\n try {\n const stored = window.localStorage.getItem(storageKey)\n return stored === \"light\" || stored === \"dark\" || stored === \"system\" ? stored : \"system\"\n } catch {\n return \"system\"\n }\n}\n\nexport function ThemeProvider({\n children,\n controller,\n theme = \"soft\",\n}: {\n children: React.ReactNode\n controller?: Agents24ChatAppearanceController\n theme?: Agents24ChatTheme\n}) {\n const [internalAppearance, setInternalAppearance] = React.useState<Agents24ChatAppearance>(\"system\")\n const [resolvedAppearance, setResolvedAppearance] = React.useState<\"light\" | \"dark\">(\"light\")\n const [mounted, setMounted] = React.useState(false)\n const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null)\n const appearance = controller?.appearance ?? internalAppearance\n\n React.useEffect(() => {\n if (theme !== \"inherit\" && !controller) setInternalAppearance(storedAppearance())\n setMounted(true)\n }, [controller, theme])\n\n React.useEffect(() => {\n if (theme === \"inherit\" || !mounted || typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n const apply = () => setResolvedAppearance(\n appearance === \"system\" ? media.matches ? \"dark\" : \"light\" : appearance,\n )\n apply()\n media.addEventListener(\"change\", apply)\n return () => media.removeEventListener(\"change\", apply)\n }, [appearance, mounted, theme])\n\n const change = React.useCallback((next: Agents24ChatAppearance) => {\n if (theme === \"inherit\") return\n if (controller) {\n controller.setAppearance(next)\n return\n }\n try { window.localStorage.setItem(storageKey, next) } catch { /* storage is optional */ }\n setInternalAppearance(next)\n }, [controller, theme])\n\n return (\n <ThemeContext.Provider value={{ appearance, inherited: theme === \"inherit\", portalContainer, setAppearance: change }}>\n <div\n ref={setPortalContainer}\n className={theme === \"inherit\" ? \"size-full\" : `${resolvedAppearance} size-full`}\n data-agents24-theme={theme}\n data-agents24-theme-root=\"\"\n >\n {children}\n </div>\n </ThemeContext.Provider>\n )\n}\n\nexport function useTheme() {\n const context = React.useContext(ThemeContext)\n if (!context) throw new Error(\"useTheme must be used inside ThemeProvider\")\n return context\n}\n\nexport function useThemePortalContainer() {\n return useTheme().portalContainer\n}\n"
109
+ "content": "import * as React from \"react\"\n\nexport type Agents24ChatAppearance = \"system\" | \"light\" | \"dark\"\nexport type Agents24ChatTheme = \"soft\" | \"sharp\"\n\nexport type Agents24ChatAppearanceController = {\n appearance: Agents24ChatAppearance\n setAppearance: (appearance: Agents24ChatAppearance) => void\n}\n\ntype ThemeContextValue = {\n inherited: boolean\n appearance: Agents24ChatAppearance\n portalContainer: HTMLElement | null\n setAppearance: (appearance: Agents24ChatAppearance) => void\n}\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null)\nconst appearanceEvent = \"agents24-chat-appearance-change\"\nconst subscribeAppearance = (notify: () => void) => {\n window.addEventListener(\"storage\", notify)\n window.addEventListener(appearanceEvent, notify)\n return () => {\n window.removeEventListener(\"storage\", notify)\n window.removeEventListener(appearanceEvent, notify)\n }\n}\nconst subscribeDarkMode = (notify: () => void) => {\n const media = window.matchMedia?.(\"(prefers-color-scheme: dark)\")\n media?.addEventListener(\"change\", notify)\n return () => media?.removeEventListener(\"change\", notify)\n}\nconst getDarkMode = () => window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches ?? false\nconst storageKey = \"agents24-chat-appearance\"\n\nfunction storedAppearance(): Agents24ChatAppearance {\n try {\n const stored = window.localStorage.getItem(storageKey)\n return stored === \"light\" || stored === \"dark\" || stored === \"system\" ? stored : \"system\"\n } catch {\n return \"system\"\n }\n}\n\nexport function ThemeProvider({\n children,\n controller,\n theme = \"soft\",\n mode = \"isolated\",\n}: {\n children: React.ReactNode\n controller?: Agents24ChatAppearanceController\n theme?: Agents24ChatTheme\n mode?: \"isolated\" | \"inherit\"\n}) {\n const [sessionAppearance, setSessionAppearance] = React.useState<Agents24ChatAppearance | null>(null)\n const stored = React.useSyncExternalStore(subscribeAppearance, storedAppearance, () => \"system\" as const)\n const systemDark = React.useSyncExternalStore(subscribeDarkMode, getDarkMode, () => false)\n const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null)\n const appearance = controller?.appearance ?? sessionAppearance ?? stored\n const resolvedAppearance = appearance === \"system\" ? (systemDark ? \"dark\" : \"light\") : appearance\n\n const change = React.useCallback((next: Agents24ChatAppearance) => {\n if (controller) {\n controller.setAppearance(next)\n return\n }\n setSessionAppearance(next)\n try { window.localStorage.setItem(storageKey, next) } catch { /* storage is optional */ }\n window.dispatchEvent(new Event(appearanceEvent))\n }, [controller])\n\n return (\n <ThemeContext.Provider value={{ appearance, portalContainer, setAppearance: change, inherited: mode === \"inherit\" }}>\n <div\n ref={setPortalContainer}\n className={mode === \"inherit\" ? \"size-full\" : `${resolvedAppearance} size-full`}\n data-agents24-theme-mode={mode}\n data-agents24-theme={theme}\n data-agents24-theme-root=\"\"\n >\n {children}\n </div>\n </ThemeContext.Provider>\n )\n}\n\nexport function useTheme() {\n const context = React.useContext(ThemeContext)\n if (!context) throw new Error(\"useTheme must be used inside ThemeProvider\")\n return context\n}\n\nexport function useThemePortalContainer() {\n return useTheme().portalContainer\n}\n"
104
110
  },
105
111
  {
106
112
  "path": "scaffold/src/components/ui/attachment.tsx",
@@ -202,7 +208,7 @@
202
208
  "path": "scaffold/src/components/ui/tooltip.tsx",
203
209
  "type": "registry:file",
204
210
  "target": "@components/agents24-chat/components/ui/tooltip.tsx",
205
- "content": "import * as React from \"react\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction TooltipProvider({ children }: { children: React.ReactNode }) {\n return children\n}\n\nfunction Tooltip({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"tooltip\"\n className={cn(\"group/tooltip relative inline-flex\", className)}\n {...props}\n />\n )\n}\n\nfunction TooltipTrigger({\n asChild: _asChild,\n ...props\n}: React.ComponentProps<typeof Slot.Root> & { asChild?: boolean }) {\n return <Slot.Root data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n className,\n align: _align,\n side = \"top\",\n sideOffset: _sideOffset,\n ...props\n}: React.ComponentProps<\"span\"> & {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n sideOffset?: number\n align?: \"start\" | \"center\" | \"end\"\n}) {\n return (\n <span\n role=\"tooltip\"\n data-side={side}\n data-slot=\"tooltip-content\"\n className={cn(\n \"pointer-events-none absolute z-50 hidden w-max max-w-xs items-center gap-1.5 rounded-xl bg-foreground px-3 py-1.5 text-xs text-background group-hover/tooltip:inline-flex group-focus-within/tooltip:inline-flex\",\n \"data-[side=top]:bottom-full data-[side=top]:left-1/2 data-[side=top]:mb-2 data-[side=top]:-translate-x-1/2\",\n \"data-[side=bottom]:top-full data-[side=bottom]:left-1/2 data-[side=bottom]:mt-2 data-[side=bottom]:-translate-x-1/2\",\n \"data-[side=left]:end-full data-[side=left]:top-1/2 data-[side=left]:me-2 data-[side=left]:-translate-y-1/2\",\n \"data-[side=right]:start-full data-[side=right]:top-1/2 data-[side=right]:ms-2 data-[side=right]:-translate-y-1/2\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }\n"
211
+ "content": "import * as React from \"react\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction TooltipProvider({ children }: { children: React.ReactNode }) {\n return children\n}\n\nfunction Tooltip({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"tooltip\"\n className={cn(\"group/tooltip relative inline-flex\", className)}\n {...props}\n />\n )\n}\n\nfunction TooltipTrigger({\n asChild: _asChild,\n ...props\n}: React.ComponentProps<typeof Slot.Root> & { asChild?: boolean }) {\n void _asChild\n return <Slot.Root data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n className,\n align: _align,\n side = \"top\",\n sideOffset: _sideOffset,\n ...props\n}: React.ComponentProps<\"span\"> & {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n sideOffset?: number\n align?: \"start\" | \"center\" | \"end\"\n}) {\n void _align\n void _sideOffset\n return (\n <span\n role=\"tooltip\"\n data-side={side}\n data-slot=\"tooltip-content\"\n className={cn(\n \"pointer-events-none absolute z-50 hidden w-max max-w-xs items-center gap-1.5 rounded-xl bg-foreground px-3 py-1.5 text-xs text-background group-hover/tooltip:inline-flex group-focus-within/tooltip:inline-flex\",\n \"data-[side=top]:bottom-full data-[side=top]:left-1/2 data-[side=top]:mb-2 data-[side=top]:-translate-x-1/2\",\n \"data-[side=bottom]:top-full data-[side=bottom]:left-1/2 data-[side=bottom]:mt-2 data-[side=bottom]:-translate-x-1/2\",\n \"data-[side=left]:end-full data-[side=left]:top-1/2 data-[side=left]:me-2 data-[side=left]:-translate-y-1/2\",\n \"data-[side=right]:start-full data-[side=right]:top-1/2 data-[side=right]:ms-2 data-[side=right]:-translate-y-1/2\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }\n"
206
212
  },
207
213
  {
208
214
  "path": "scaffold/src/hooks/use-mobile.ts",
@@ -210,6 +216,12 @@
210
216
  "target": "@components/agents24-chat/hooks/use-mobile.ts",
211
217
  "content": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n React.useEffect(() => {\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n }\n if (typeof window.matchMedia !== \"function\") {\n onChange()\n window.addEventListener(\"resize\", onChange)\n return () => window.removeEventListener(\"resize\", onChange)\n }\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n mql.addEventListener(\"change\", onChange)\n onChange()\n return () => mql.removeEventListener(\"change\", onChange)\n }, [])\n\n return !!isMobile\n}\n"
212
218
  },
219
+ {
220
+ "path": "scaffold/src/host.css",
221
+ "type": "registry:file",
222
+ "target": "@components/agents24-chat/host.css",
223
+ "content": "@layer theme, base, components, utilities;\n@import \"tailwindcss/theme.css\" layer(theme);\n@import \"tailwindcss/utilities.css\" layer(utilities);\n@import \"tw-animate-css\";\n@import \"@fontsource-variable/jetbrains-mono\";\n@import \"@fontsource-variable/merriweather\";\n@import \"@fontsource-variable/inter\";\n\n@source \"./**/*.{ts,tsx}\";\n\n@import \"./theme.css\";\n"
224
+ },
213
225
  {
214
226
  "path": "scaffold/src/identity.ts",
215
227
  "type": "registry:file",
@@ -220,7 +232,7 @@
220
232
  "path": "scaffold/src/index.tsx",
221
233
  "type": "registry:file",
222
234
  "target": "@components/agents24-chat/index.tsx",
223
- "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { App, type Agents24ChatAppProps } from \"./app\"\nimport {\n ThemeProvider,\n type Agents24ChatAppearanceController,\n type Agents24ChatTheme,\n} from \"./components/theme-provider\"\nimport { TooltipProvider } from \"./components/ui/tooltip\"\n\nexport type Agents24ChatScaffoldProps = Agents24ChatAppProps & {\n appearanceController?: Agents24ChatAppearanceController\n theme?: Agents24ChatTheme\n}\n\nexport function Agents24ChatScaffold({ appearanceController, theme = \"inherit\", ...props }: Agents24ChatScaffoldProps) {\n return (\n <ThemeProvider controller={appearanceController} theme={theme}>\n <TooltipProvider>\n <App {...props} />\n </TooltipProvider>\n </ThemeProvider>\n )\n}\n\nexport { App as Agents24ChatScaffoldApp }\nexport { ThemeProvider as Agents24ChatThemeProvider }\nexport type {\n Agents24ChatAppearance,\n Agents24ChatAppearanceController,\n Agents24ChatTheme,\n} from \"./components/theme-provider\"\nexport type { Agents24ChatIdentity, Agents24ChatIdentityState } from \"./identity\"\nexport type {\n ChatHitlResponse,\n Agents24ChatScaffoldComponents,\n Agents24ChatTimelineSlotProps,\n} from \"./app\"\nexport { TooltipProvider as Agents24ChatTooltipProvider }\n\nexport { ChatComposer as Agents24ChatComposer } from \"./components/chat/chat-composer\"\nexport type { Agents24ChatComposerProps } from \"./components/chat/chat-composer\"\n"
235
+ "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { App, type Agents24ChatAppProps } from \"./app\"\nimport {\n ThemeProvider,\n type Agents24ChatAppearanceController,\n type Agents24ChatTheme,\n} from \"./components/theme-provider\"\nimport { TooltipProvider } from \"./components/ui/tooltip\"\n\nexport type Agents24ChatScaffoldProps = Agents24ChatAppProps & {\n appearanceController?: Agents24ChatAppearanceController\n theme?: Agents24ChatTheme\n themeMode?: \"isolated\" | \"inherit\"\n}\n\nexport function Agents24ChatScaffold({ appearanceController, theme, themeMode = \"inherit\", ...props }: Agents24ChatScaffoldProps) {\n return (\n <ThemeProvider controller={appearanceController} theme={theme} mode={themeMode}>\n <TooltipProvider>\n <App {...props} />\n </TooltipProvider>\n </ThemeProvider>\n )\n}\n\nexport { App as Agents24ChatScaffoldApp }\nexport { ThemeProvider as Agents24ChatThemeProvider }\nexport type {\n Agents24ChatAppearance,\n Agents24ChatAppearanceController,\n Agents24ChatTheme,\n} from \"./components/theme-provider\"\nexport type { Agents24ChatIdentity, Agents24ChatIdentityState } from \"./identity\"\nexport type {\n ChatHitlResponse,\n Agents24ChatScaffoldComponents,\n Agents24ChatTimelineSlotProps,\n Agents24ChatComposerSlotProps,\n} from \"./app\"\nexport { TooltipProvider as Agents24ChatTooltipProvider }\n\nexport { ChatComposer as Agents24ChatComposer } from \"./components/chat/chat-composer\"\nexport type { Agents24ChatComposerProps } from \"./components/chat/chat-composer\"\n"
224
236
  },
225
237
  {
226
238
  "path": "scaffold/src/lib/utils.ts",
@@ -228,6 +240,12 @@
228
240
  "target": "@components/agents24-chat/lib/utils.ts",
229
241
  "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"
230
242
  },
243
+ {
244
+ "path": "scaffold/src/theme.css",
245
+ "type": "registry:file",
246
+ "target": "@components/agents24-chat/theme.css",
247
+ "content": "@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --font-heading: var(--a24-font-heading);\n --font-sans: var(--a24-font-sans);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --color-foreground: var(--foreground);\n --color-background: var(--background);\n --radius-sm: calc(var(--radius) * 0.6);\n --radius-md: calc(var(--radius) * 0.8);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) * 1.4);\n --radius-2xl: calc(var(--radius) * 1.8);\n --radius-3xl: calc(var(--radius) * 2.2);\n --radius-4xl: calc(var(--radius) * 2.6);\n --a24-composer-single-row-radius: 9999px;\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --font-serif: var(--a24-font-heading);\n --font-mono: var(--a24-font-mono);\n}\n\n[data-agents24-theme-root] {\n color-scheme: light;\n --a24-font-heading: 'Inter Variable', sans-serif;\n --a24-font-sans: 'Inter Variable', sans-serif;\n --a24-font-mono: 'JetBrains Mono Variable', monospace;\n --radius-sm: calc(var(--radius) * 0.6);\n --radius-md: calc(var(--radius) * 0.8);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) * 1.4);\n --radius-2xl: calc(var(--radius) * 1.8);\n --radius-3xl: calc(var(--radius) * 2.2);\n --radius-4xl: calc(var(--radius) * 2.6);\n --background: oklch(1 0 0);\n --foreground: oklch(0.147 0.004 49.25);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.147 0.004 49.25);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.147 0.004 49.25);\n --primary: oklch(0.52 0.105 223.128);\n --primary-foreground: oklch(0.984 0.019 200.873);\n --secondary: oklch(0.967 0.001 286.375);\n --secondary-foreground: oklch(0.21 0.006 285.885);\n --muted: oklch(0.97 0.001 106.424);\n --muted-foreground: oklch(0.553 0.013 58.071);\n --accent: oklch(0.97 0.001 106.424);\n --accent-foreground: oklch(0.216 0.006 56.043);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.923 0.003 48.717);\n --input: oklch(0.923 0.003 48.717);\n --ring: oklch(0.709 0.01 56.259);\n --radius: 0.875rem;\n --sidebar: oklch(0.985 0.001 106.423);\n --sidebar-foreground: oklch(0.147 0.004 49.25);\n --sidebar-primary: oklch(0.609 0.126 221.723);\n --sidebar-primary-foreground: oklch(0.984 0.019 200.873);\n --sidebar-accent: oklch(0.97 0.001 106.424);\n --sidebar-accent-foreground: oklch(0.216 0.006 56.043);\n --sidebar-border: oklch(0.923 0.003 48.717);\n --sidebar-ring: oklch(0.709 0.01 56.259);\n --chart-1: oklch(0.845 0.143 164.978);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.596 0.145 163.225);\n --chart-4: oklch(0.508 0.118 165.612);\n --chart-5: oklch(0.432 0.095 166.913);\n}\n\n[data-agents24-theme-root].dark,\n.dark [data-agents24-theme-root][data-agents24-theme-mode=\"inherit\"] {\n color-scheme: dark;\n --background: oklch(0.147 0.004 49.25);\n --foreground: oklch(0.985 0.001 106.423);\n --card: oklch(0.216 0.006 56.043);\n --card-foreground: oklch(0.985 0.001 106.423);\n --popover: oklch(0.216 0.006 56.043);\n --popover-foreground: oklch(0.985 0.001 106.423);\n --primary: oklch(0.45 0.085 224.283);\n --primary-foreground: oklch(0.984 0.019 200.873);\n --secondary: oklch(0.274 0.006 286.033);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.268 0.007 34.298);\n --muted-foreground: oklch(0.709 0.01 56.259);\n --accent: oklch(0.268 0.007 34.298);\n --accent-foreground: oklch(0.985 0.001 106.423);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.553 0.013 58.071);\n --sidebar: oklch(0.216 0.006 56.043);\n --sidebar-foreground: oklch(0.985 0.001 106.423);\n --sidebar-primary: oklch(0.715 0.143 215.221);\n --sidebar-primary-foreground: oklch(0.302 0.056 229.695);\n --sidebar-accent: oklch(0.268 0.007 34.298);\n --sidebar-accent-foreground: oklch(0.985 0.001 106.423);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.553 0.013 58.071);\n --chart-1: oklch(0.845 0.143 164.978);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.596 0.145 163.225);\n --chart-4: oklch(0.508 0.118 165.612);\n --chart-5: oklch(0.432 0.095 166.913);\n}\n\n[data-agents24-theme-root][data-agents24-theme=\"sharp\"] {\n --a24-composer-single-row-radius: 0;\n --a24-font-heading: 'Merriweather Variable', serif;\n --a24-font-sans: 'JetBrains Mono Variable', monospace;\n --a24-font-mono: 'JetBrains Mono Variable', monospace;\n --background: oklch(1 0 0);\n --foreground: oklch(0.141 0.005 285.823);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.141 0.005 285.823);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.141 0.005 285.823);\n --primary: oklch(0.21 0.006 285.885);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.967 0.001 286.375);\n --secondary-foreground: oklch(0.21 0.006 285.885);\n --muted: oklch(0.967 0.001 286.375);\n --muted-foreground: oklch(0.552 0.016 285.938);\n --accent: oklch(0.967 0.001 286.375);\n --accent-foreground: oklch(0.21 0.006 285.885);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.92 0.004 286.32);\n --input: oklch(0.92 0.004 286.32);\n --ring: oklch(0.705 0.015 286.067);\n --radius: 0;\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.141 0.005 285.823);\n --sidebar-primary: oklch(0.21 0.006 285.885);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.967 0.001 286.375);\n --sidebar-accent-foreground: oklch(0.21 0.006 285.885);\n --sidebar-border: oklch(0.92 0.004 286.32);\n --sidebar-ring: oklch(0.705 0.015 286.067);\n --chart-1: oklch(0.865 0.127 207.078);\n --chart-2: oklch(0.715 0.143 215.221);\n --chart-3: oklch(0.609 0.126 221.723);\n --chart-4: oklch(0.52 0.105 223.128);\n --chart-5: oklch(0.45 0.085 224.283);\n}\n\n[data-agents24-theme-root][data-agents24-theme=\"sharp\"].dark,\n.dark [data-agents24-theme-root][data-agents24-theme=\"sharp\"][data-agents24-theme-mode=\"inherit\"] {\n --background: oklch(0.141 0.005 285.823);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.21 0.006 285.885);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.21 0.006 285.885);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.92 0.004 286.32);\n --primary-foreground: oklch(0.21 0.006 285.885);\n --secondary: oklch(0.274 0.006 286.033);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.274 0.006 286.033);\n --muted-foreground: oklch(0.705 0.015 286.067);\n --accent: oklch(0.274 0.006 286.033);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.552 0.016 285.938);\n --sidebar: oklch(0.21 0.006 285.885);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.274 0.006 286.033);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.552 0.016 285.938);\n --chart-1: oklch(0.865 0.127 207.078);\n --chart-2: oklch(0.715 0.143 215.221);\n --chart-3: oklch(0.609 0.126 221.723);\n --chart-4: oklch(0.52 0.105 223.128);\n --chart-5: oklch(0.45 0.085 224.283);\n}\n\n@layer base {\n [data-agents24-theme-root], [data-agents24-theme-root] * {\n @apply border-border outline-ring/50;\n box-sizing: border-box;\n }\n [data-agents24-theme-root] {\n @apply size-full overflow-hidden bg-background font-sans text-foreground antialiased;\n }\n [data-agents24-theme-root] button,\n [data-agents24-theme-root] textarea { font: inherit; }\n}\n"
248
+ },
231
249
  {
232
250
  "path": "scaffold/styles.css",
233
251
  "type": "registry:file",
@@ -236,13 +254,13 @@
236
254
  }
237
255
  ],
238
256
  "meta": {
239
- "version": "0.5.30",
257
+ "version": "0.5.32",
240
258
  "source": "packages/agents24-chat-react/scaffold",
241
- "source_sha256": "9158778b457fa19601f247c37ac7353d613b81deb86509aa8fa4e5cee564672c",
259
+ "source_sha256": "9947c6eb88ebdea4ae6213cdd21002e3b1feb13111bcde7388deb5452fa0786d",
242
260
  "agents24": {
243
- "@agents24/client": "0.2.14",
244
- "@agents24/react": "0.2.18",
245
- "@agents24/chat-react": "0.5.30"
261
+ "@agents24/client": "0.2.15",
262
+ "@agents24/react": "0.2.19",
263
+ "@agents24/chat-react": "0.5.32"
246
264
  }
247
265
  },
248
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."