create-agents24-app 0.2.26 → 0.2.27

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.
@@ -0,0 +1,4 @@
1
+ @import "@agents24/chat-react/styles.css";
2
+ @import "tw-animate-css";
3
+ @import "./src/chat.css";
4
+ @source "./src/**/*.{ts,tsx}";
@@ -15,6 +15,7 @@ import {
15
15
  } from "@agents24/react"
16
16
  import * as React from "react"
17
17
 
18
+ import type { Agents24ChatComposerProps } from "./components/chat/chat-composer"
18
19
  import { ChatShell } from "./components/chat/chat-shell"
19
20
  import { GUEST_CHAT_IDENTITY, type Agents24ChatIdentity } from "./identity"
20
21
 
@@ -37,6 +38,7 @@ export type Agents24ChatAppProps = {
37
38
 
38
39
  export type Agents24ChatScaffoldComponents = {
39
40
  Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
41
+ Composer?: React.ComponentType<Agents24ChatComposerProps>
40
42
  }
41
43
 
42
44
  export type ChatHitlResponse = HitlResponse
@@ -64,3 +64,27 @@
64
64
  display: -webkit-box;
65
65
  overflow: hidden;
66
66
  }
67
+
68
+ /* Host theme mode changes no document tokens or appearance. */
69
+ [data-agents24-theme-root][data-agents24-theme="inherit"] {
70
+ --a24-composer-single-row-radius: var(--radius-xl, var(--radius, .75rem));
71
+ font-family: inherit;
72
+ }
73
+ [data-agents24-theme-root][data-agents24-theme="inherit"] :focus-visible {
74
+ outline: 2px solid var(--ring, currentColor) !important;
75
+ outline-offset: 2px;
76
+ }
77
+ [data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover {
78
+ background: var(--popover, var(--background));
79
+ color: var(--popover-foreground, var(--foreground));
80
+ }
81
+
82
+ [data-agents24-theme-root][data-agents24-theme="inherit"] [data-slot="button"] {
83
+ border-radius: var(--radius);
84
+ }
85
+ [data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover {
86
+ border-color: var(--border);
87
+ }
88
+ [data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover > span:not(.a24-context-status__sr-only) {
89
+ color: var(--muted-foreground);
90
+ }
@@ -3,14 +3,10 @@ import * as React from "react"
3
3
  import {
4
4
  AgentChatContextStatus,
5
5
  ChatAttachmentRows,
6
- chatComposerAction,
7
- createAgentChatComposerFile,
8
- revokeAgentChatComposerFiles,
9
- useAudioRecorder,
10
- useChatComposerFocus,
6
+ useAgentChatComposer,
11
7
  type AgentChatComposerFile,
12
8
  } from "@agents24/chat-react/ui"
13
- import { attachmentFileAccept, inputMimeTypes, validateAgentChatSubmission, type AgentChatInputContract } from "@agents24/chat-react/runtime"
9
+ import { type AgentChatInputContract } from "@agents24/chat-react/runtime"
14
10
  import type { ContextWindow } from "@agents24/client/protocol"
15
11
 
16
12
  import { Button } from "../ui/button"
@@ -23,7 +19,7 @@ import { Textarea } from "../ui/textarea"
23
19
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
24
20
  import { cn } from "../../lib/utils"
25
21
 
26
- type ChatComposerProps = {
22
+ export type Agents24ChatComposerProps = {
27
23
  inputContract: AgentChatInputContract
28
24
  disabled?: boolean
29
25
  contextWindow?: ContextWindow | null
@@ -61,45 +57,14 @@ export function ChatComposer({
61
57
  onSubmit,
62
58
  restoredDraft,
63
59
  utilityRow = "inline",
64
- }: ChatComposerProps) {
65
- const [text, setText] = React.useState("")
66
- const [files, setFiles] = React.useState<AgentChatComposerFile[]>([])
67
- const [attachmentError, setAttachmentError] = React.useState<string | null>(null)
60
+ }: Agents24ChatComposerProps) {
68
61
  const inputRef = React.useRef<HTMLInputElement>(null)
69
62
  const textareaRef = React.useRef<HTMLTextAreaElement>(null)
70
- const filesRef = React.useRef<AgentChatComposerFile[]>([])
71
- const inFlightFilesRef = React.useRef<AgentChatComposerFile[]>([])
72
- const submittingRef = React.useRef(false)
73
- const draftRevisionRef = React.useRef(0)
74
- const attachmentMimeTypes = inputMimeTypes(inputContract)
75
- const allowAttachments = attachmentMimeTypes.length > 0
76
- const recordingAllowed = inputContract.modalities.audio.recording_enabled
77
- React.useEffect(() => {
78
- filesRef.current = files
79
- }, [files])
80
- React.useEffect(() => () => {
81
- revokeAgentChatComposerFiles(filesRef.current)
82
- revokeAgentChatComposerFiles(inFlightFilesRef.current)
83
- }, [])
84
- const focusComposer = useChatComposerFocus({ textareaRef, disabled: Boolean(disabled), focusKey, restoredDraft,
85
- onRestore: (draft) => {
86
- draftRevisionRef.current += 1
87
- revokeAgentChatComposerFiles(filesRef.current)
88
- setText(draft.text)
89
- setFiles(draft.files)
90
- } })
91
- const addRecording = React.useCallback((file: File) => {
92
- draftRevisionRef.current += 1
93
- setFiles((current) => [...current, createAgentChatComposerFile(file)])
94
- }, [])
95
- const recorder = useAudioRecorder(addRecording)
63
+ const composer = useAgentChatComposer({ inputContract, disabled, isRunning, isCancelling,
64
+ focusKey, restoredDraft, textareaRef, onSubmit: ({ text, files }) => onSubmit(text, files) })
65
+ const { text, files, setText, validationError, attachmentError, recorder, recordingAllowed,
66
+ allowAttachments, action, submit } = composer
96
67
  const isExpanded = forceExpanded || text.includes("\n") || text.length > 62
97
- const validationError = validateAgentChatSubmission(
98
- inputContract,
99
- text,
100
- files.map((file) => ({ data: file.source || new Uint8Array(), mediaType: file.mediaType })),
101
- )
102
- const action = chatComposerAction({ hasDraft: Boolean(text.trim() || files.length), isRunning, isCancelling })
103
68
 
104
69
  const selectedAgent = agents.find((item) => item.id === agentId)
105
70
  const selectedModel = models.find((item) => item.id === modelId)
@@ -159,36 +124,6 @@ export function ChatComposer({
159
124
  const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0
160
125
  const showUtilityRow = utilityRow === "below"
161
126
 
162
- const submit = async () => {
163
- if (disabled || isCancelling || submittingRef.current || validationError) return
164
- const value = text
165
- const selectedFiles = files
166
- const clearedRevision = draftRevisionRef.current + 1
167
- draftRevisionRef.current = clearedRevision
168
- submittingRef.current = true
169
- inFlightFilesRef.current = selectedFiles
170
- setText("")
171
- setFiles([])
172
- focusComposer()
173
- try {
174
- await onSubmit(value, selectedFiles)
175
- revokeAgentChatComposerFiles(selectedFiles)
176
- inFlightFilesRef.current = []
177
- } catch {
178
- if (draftRevisionRef.current === clearedRevision) {
179
- draftRevisionRef.current += 1
180
- setText(value)
181
- setFiles(selectedFiles)
182
- inFlightFilesRef.current = []
183
- } else {
184
- revokeAgentChatComposerFiles(selectedFiles)
185
- inFlightFilesRef.current = []
186
- }
187
- } finally {
188
- submittingRef.current = false
189
- }
190
- }
191
-
192
127
  return (
193
128
  <div
194
129
  className="relative z-10 w-full pb-[max(1rem,env(safe-area-inset-bottom))]"
@@ -198,15 +133,7 @@ export function ChatComposer({
198
133
  <div className="mb-2 min-w-0">
199
134
  <ChatAttachmentRows
200
135
  attachments={files}
201
- onRemove={(attachment) => {
202
- if (!attachment.id) return
203
- draftRevisionRef.current += 1
204
- setFiles((current) => {
205
- const removed = current.find((file) => file.id === attachment.id)
206
- if (removed) revokeAgentChatComposerFiles([removed])
207
- return current.filter((file) => file.id !== attachment.id)
208
- })
209
- }}
136
+ onRemove={(attachment) => { if (attachment.id) composer.removeFile(attachment.id) }}
210
137
  />
211
138
  </div>
212
139
  )}
@@ -225,11 +152,9 @@ export function ChatComposer({
225
152
  className="sr-only"
226
153
  type="file"
227
154
  multiple
228
- accept={attachmentFileAccept(attachmentMimeTypes)}
155
+ accept={composer.fileAccept}
229
156
  onChange={(event) => {
230
- const selected = Array.from(event.target.files ?? [])
231
- setFiles((current) => [...current, ...selected.map(createAgentChatComposerFile)])
232
- setAttachmentError(null)
157
+ composer.addFiles(Array.from(event.target.files ?? []))
233
158
  event.currentTarget.value = ""
234
159
  }}
235
160
  />
@@ -251,16 +176,8 @@ export function ChatComposer({
251
176
  "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",
252
177
  isExpanded ? "py-2" : "h-9 py-2",
253
178
  )}
254
- onChange={(event) => {
255
- draftRevisionRef.current += 1
256
- setText(event.target.value)
257
- }}
258
- onKeyDown={(event) => {
259
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
260
- event.preventDefault()
261
- void submit()
262
- }
263
- }}
179
+ onChange={(event) => setText(event.target.value)}
180
+ onKeyDown={composer.onKeyDown}
264
181
  />
265
182
  <div className="agents24-composer-actions flex items-center gap-1">
266
183
  {utilityRow === "inline" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}
@@ -291,7 +208,7 @@ export function ChatComposer({
291
208
  <TooltipContent>{validationError}</TooltipContent>
292
209
  </Tooltip>
293
210
  ) : (
294
- <Button size="icon" aria-label={isRunning ? "Queue message" : "Send message"} disabled={disabled} onClick={() => void submit()}><ArrowUp /></Button>
211
+ <Button size="icon" aria-label={isRunning ? "Queue message" : "Send message"} disabled={disabled || !composer.canSubmit} onClick={() => void submit()}><ArrowUp /></Button>
295
212
  )}
296
213
  </div>
297
214
  </div>
@@ -92,6 +92,7 @@ export function ChatShell({
92
92
  runtime: AgentChatRuntime
93
93
  sidebar?: React.ReactNode
94
94
  }) {
95
+ const Composer = components?.Composer || ChatComposer
95
96
  const { controller } = runtime
96
97
  const isUploading = controller.runState === "uploading"
97
98
  const isRunning =
@@ -243,7 +244,7 @@ export function ChatShell({
243
244
  canSteer={controller.runState === "streaming"}
244
245
  onSteer={controller.steerPendingMessage} onRemove={controller.removePendingMessage}
245
246
  onEdit={retractForEdit} onResume={controller.resumeQueue} />
246
- <ChatComposer
247
+ <Composer
247
248
  restoredDraft={restoredDraft}
248
249
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
249
250
  contextWindow={runtime.contextWindow}
@@ -11,7 +11,7 @@ const options = [
11
11
  ]
12
12
 
13
13
  export function ThemeMenu() {
14
- const { appearance, setAppearance } = useTheme()
14
+ const { appearance, setAppearance, inherited } = useTheme()
15
15
  const [open, setOpen] = React.useState(false)
16
16
  const rootRef = React.useRef<HTMLDivElement>(null)
17
17
  const firstItemRef = React.useRef<HTMLButtonElement>(null)
@@ -35,6 +35,8 @@ export function ThemeMenu() {
35
35
  }
36
36
  }, [open])
37
37
 
38
+ if (inherited) return null
39
+
38
40
  const label = appearance === "system" ? "System" : appearance === "light" ? "Light" : "Dark"
39
41
  return (
40
42
  <div ref={rootRef} className="relative">
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
 
3
3
  export type Agents24ChatAppearance = "system" | "light" | "dark"
4
- export type Agents24ChatTheme = "soft" | "sharp"
4
+ export type Agents24ChatTheme = "soft" | "sharp" | "inherit"
5
5
 
6
6
  export type Agents24ChatAppearanceController = {
7
7
  appearance: Agents24ChatAppearance
@@ -10,6 +10,7 @@ export type Agents24ChatAppearanceController = {
10
10
 
11
11
  type ThemeContextValue = {
12
12
  appearance: Agents24ChatAppearance
13
+ inherited: boolean
13
14
  portalContainer: HTMLElement | null
14
15
  setAppearance: (appearance: Agents24ChatAppearance) => void
15
16
  }
@@ -42,12 +43,12 @@ export function ThemeProvider({
42
43
  const appearance = controller?.appearance ?? internalAppearance
43
44
 
44
45
  React.useEffect(() => {
45
- if (!controller) setInternalAppearance(storedAppearance())
46
+ if (theme !== "inherit" && !controller) setInternalAppearance(storedAppearance())
46
47
  setMounted(true)
47
- }, [controller])
48
+ }, [controller, theme])
48
49
 
49
50
  React.useEffect(() => {
50
- if (!mounted || typeof window === "undefined" || typeof window.matchMedia !== "function") return
51
+ if (theme === "inherit" || !mounted || typeof window === "undefined" || typeof window.matchMedia !== "function") return
51
52
  const media = window.matchMedia("(prefers-color-scheme: dark)")
52
53
  const apply = () => setResolvedAppearance(
53
54
  appearance === "system" ? media.matches ? "dark" : "light" : appearance,
@@ -55,22 +56,23 @@ export function ThemeProvider({
55
56
  apply()
56
57
  media.addEventListener("change", apply)
57
58
  return () => media.removeEventListener("change", apply)
58
- }, [appearance, mounted])
59
+ }, [appearance, mounted, theme])
59
60
 
60
61
  const change = React.useCallback((next: Agents24ChatAppearance) => {
62
+ if (theme === "inherit") return
61
63
  if (controller) {
62
64
  controller.setAppearance(next)
63
65
  return
64
66
  }
65
67
  try { window.localStorage.setItem(storageKey, next) } catch { /* storage is optional */ }
66
68
  setInternalAppearance(next)
67
- }, [controller])
69
+ }, [controller, theme])
68
70
 
69
71
  return (
70
- <ThemeContext.Provider value={{ appearance, portalContainer, setAppearance: change }}>
72
+ <ThemeContext.Provider value={{ appearance, inherited: theme === "inherit", portalContainer, setAppearance: change }}>
71
73
  <div
72
74
  ref={setPortalContainer}
73
- className={`${resolvedAppearance} size-full`}
75
+ className={theme === "inherit" ? "size-full" : `${resolvedAppearance} size-full`}
74
76
  data-agents24-theme={theme}
75
77
  data-agents24-theme-root=""
76
78
  >
@@ -54,7 +54,7 @@
54
54
  --font-mono: var(--a24-font-mono);
55
55
  }
56
56
 
57
- [data-agents24-theme-root] {
57
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]) {
58
58
  color-scheme: light;
59
59
  --a24-font-heading: 'Inter Variable', sans-serif;
60
60
  --a24-font-sans: 'Inter Variable', sans-serif;
@@ -100,7 +100,7 @@
100
100
  --chart-5: oklch(0.432 0.095 166.913);
101
101
  }
102
102
 
103
- [data-agents24-theme-root].dark {
103
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]).dark {
104
104
  color-scheme: dark;
105
105
  --background: oklch(0.147 0.004 49.25);
106
106
  --foreground: oklch(0.985 0.001 106.423);
@@ -135,7 +135,7 @@
135
135
  --chart-5: oklch(0.432 0.095 166.913);
136
136
  }
137
137
 
138
- [data-agents24-theme-root][data-agents24-theme="sharp"] {
138
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"])[data-agents24-theme="sharp"] {
139
139
  --a24-composer-single-row-radius: 0;
140
140
  --a24-font-heading: 'Merriweather Variable', serif;
141
141
  --a24-font-sans: 'JetBrains Mono Variable', monospace;
@@ -174,7 +174,7 @@
174
174
  --chart-5: oklch(0.45 0.085 224.283);
175
175
  }
176
176
 
177
- [data-agents24-theme-root][data-agents24-theme="sharp"].dark {
177
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"])[data-agents24-theme="sharp"].dark {
178
178
  --background: oklch(0.141 0.005 285.823);
179
179
  --foreground: oklch(0.985 0 0);
180
180
  --card: oklch(0.21 0.006 285.885);
@@ -209,13 +209,13 @@
209
209
  }
210
210
 
211
211
  @layer base {
212
- [data-agents24-theme-root], [data-agents24-theme-root] * {
212
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]), [data-agents24-theme-root]:not([data-agents24-theme="inherit"]) * {
213
213
  @apply border-border outline-ring/50;
214
214
  box-sizing: border-box;
215
215
  }
216
- [data-agents24-theme-root] {
216
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]) {
217
217
  @apply size-full overflow-hidden bg-background font-sans text-foreground antialiased;
218
218
  }
219
- [data-agents24-theme-root] button,
220
- [data-agents24-theme-root] textarea { font: inherit; }
219
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]) button,
220
+ [data-agents24-theme-root]:not([data-agents24-theme="inherit"]) textarea { font: inherit; }
221
221
  }
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
4
 
3
5
  import { App, type Agents24ChatAppProps } from "./app"
@@ -37,3 +39,6 @@ export type {
37
39
  Agents24ChatTimelineSlotProps,
38
40
  } from "./app"
39
41
  export { TooltipProvider as Agents24ChatTooltipProvider }
42
+
43
+ export { ChatComposer as Agents24ChatComposer } from "./components/chat/chat-composer"
44
+ export type { Agents24ChatComposerProps } from "./components/chat/chat-composer"
@@ -20,9 +20,10 @@
20
20
  "tailwindcss": "^4.1.16"
21
21
  },
22
22
  "source": "packages/agents24-chat-react/scaffold",
23
- "sha256": "29777d11a1fbf6d033080468041b2368cab1572f30c46e246e65725e3b32bcad",
23
+ "sha256": "ff79a5c99abb98c547132058504c6404d1cf9513f63ad98aaf1dfafc2dad4688",
24
24
  "files": [
25
25
  "components.json",
26
+ "inherit.css",
26
27
  "manifest.json",
27
28
  "src/app.tsx",
28
29
  "src/chat.css",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agents24-app",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Create a production-ready Agents24 React application.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/versions.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "agents24": {
3
3
  "@agents24/client": "^0.2.14",
4
4
  "@agents24/react": "^0.2.18",
5
- "@agents24/chat-react": "^0.5.29",
5
+ "@agents24/chat-react": "^0.5.30",
6
6
  "@agents24/node": "^0.5.19"
7
7
  },
8
8
  "runtime": {