@agents24/chat-react 0.5.5 → 0.5.7

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 (35) hide show
  1. package/dist/index.cjs +11 -6
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +11 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/latest-thread-scroller.d.ts +2 -1
  6. package/dist/runtime/index.cjs +17 -11
  7. package/dist/runtime/index.cjs.map +1 -1
  8. package/dist/runtime/index.js +17 -11
  9. package/dist/runtime/index.js.map +1 -1
  10. package/dist/runtime/use-agent-chat-runtime.d.ts +7 -2
  11. package/dist/scaffold/components/chat/chat-composer.d.ts +2 -1
  12. package/dist/scaffold/components/chat/chat-message.d.ts +3 -1
  13. package/dist/scaffold/components/chat/turn-outline.d.ts +1 -1
  14. package/dist/scaffold/index.cjs +195 -154
  15. package/dist/scaffold/index.cjs.map +1 -1
  16. package/dist/scaffold/index.js +215 -168
  17. package/dist/scaffold/index.js.map +1 -1
  18. package/dist/styles.css +25 -6
  19. package/dist/ui/adapters.d.ts +14 -2
  20. package/dist/ui/agent-chat-composer.d.ts +2 -9
  21. package/dist/ui/agent-chat-message.d.ts +7 -1
  22. package/dist/ui/composer-attachments.d.ts +11 -0
  23. package/dist/ui/index.cjs +781 -389
  24. package/dist/ui/index.cjs.map +1 -1
  25. package/dist/ui/index.d.ts +1 -0
  26. package/dist/ui/index.js +766 -375
  27. package/dist/ui/index.js.map +1 -1
  28. package/package.json +4 -3
  29. package/scaffold/src/components/chat/chat-composer.tsx +43 -19
  30. package/scaffold/src/components/chat/chat-message.tsx +19 -10
  31. package/scaffold/src/components/chat/chat-shell.tsx +65 -34
  32. package/scaffold/src/components/chat/turn-outline.tsx +17 -59
  33. package/scaffold/src/components/ui/message-scroller.tsx +1 -1
  34. package/scaffold/src/components/ui/message.tsx +1 -1
  35. package/scaffold/src/components/ui/sidebar.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents24/chat-react",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Accessible web UI and complete default layout for Agents24 chat applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,8 +58,9 @@
58
58
  "prepack": "pnpm run build && pnpm run check:boundaries"
59
59
  },
60
60
  "dependencies": {
61
- "@agents24/client": "0.2.3",
62
- "@agents24/react": "0.2.3",
61
+ "@untitledui/file-icons": "0.0.9",
62
+ "@agents24/client": "0.2.4",
63
+ "@agents24/react": "0.2.4",
63
64
  "@fontsource-variable/inter": "^5.3.0",
64
65
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
65
66
  "@fontsource-variable/merriweather": "^5.3.0",
@@ -1,6 +1,13 @@
1
- import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square, X } from "lucide-react"
1
+ import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square } from "lucide-react"
2
2
  import * as React from "react"
3
- import { AgentChatContextStatus, useAudioRecorder } from "@agents24/chat-react/ui"
3
+ import {
4
+ AgentChatContextStatus,
5
+ ChatAttachmentRows,
6
+ createAgentChatComposerFile,
7
+ revokeAgentChatComposerFiles,
8
+ useAudioRecorder,
9
+ type AgentChatComposerFile,
10
+ } from "@agents24/chat-react/ui"
4
11
  import { canonicalInputMimeType, inputMimeTypes, validateAgentChatSubmission, type AgentChatInputContract } from "@agents24/chat-react/runtime"
5
12
  import type { ContextWindow } from "@agents24/client/protocol"
6
13
 
@@ -29,7 +36,7 @@ type ChatComposerProps = {
29
36
  onModelChange: (modelId: string) => void
30
37
  onAgentChange: (agentId: string) => void
31
38
  onStop: () => void
32
- onSubmit: (text: string, files: File[]) => Promise<void>
39
+ onSubmit: (text: string, files: AgentChatComposerFile[]) => Promise<void>
33
40
  utilityRow?: "inline" | "below"
34
41
  }
35
42
 
@@ -52,15 +59,24 @@ export function ChatComposer({
52
59
  utilityRow = "inline",
53
60
  }: ChatComposerProps) {
54
61
  const [text, setText] = React.useState("")
55
- const [files, setFiles] = React.useState<File[]>([])
62
+ const [files, setFiles] = React.useState<AgentChatComposerFile[]>([])
56
63
  const [attachmentError, setAttachmentError] = React.useState<string | null>(null)
57
64
  const inputRef = React.useRef<HTMLInputElement>(null)
58
65
  const textareaRef = React.useRef<HTMLTextAreaElement>(null)
66
+ const filesRef = React.useRef<AgentChatComposerFile[]>([])
67
+ const inFlightFilesRef = React.useRef<AgentChatComposerFile[]>([])
59
68
  const submittingRef = React.useRef(false)
60
69
  const draftRevisionRef = React.useRef(0)
61
70
  const attachmentMimeTypes = inputMimeTypes(inputContract)
62
71
  const allowAttachments = attachmentMimeTypes.length > 0
63
72
  const recordingAllowed = inputContract.modalities.audio.recording_enabled
73
+ React.useEffect(() => {
74
+ filesRef.current = files
75
+ }, [files])
76
+ React.useEffect(() => () => {
77
+ revokeAgentChatComposerFiles(filesRef.current)
78
+ revokeAgentChatComposerFiles(inFlightFilesRef.current)
79
+ }, [])
64
80
  React.useEffect(() => {
65
81
  if (disabled || isRunning) return
66
82
  const frame = window.requestAnimationFrame(() => textareaRef.current?.focus({ preventScroll: true }))
@@ -68,14 +84,14 @@ export function ChatComposer({
68
84
  }, [disabled, focusKey, isRunning])
69
85
  const addRecording = React.useCallback((file: File) => {
70
86
  draftRevisionRef.current += 1
71
- setFiles((current) => [...current, file])
87
+ setFiles((current) => [...current, createAgentChatComposerFile(file)])
72
88
  }, [])
73
89
  const recorder = useAudioRecorder(addRecording)
74
90
  const isExpanded = forceExpanded || text.includes("\n") || text.length > 62
75
91
  const validationError = validateAgentChatSubmission(
76
92
  inputContract,
77
93
  text,
78
- files.map((file) => ({ data: file, mediaType: file.type })),
94
+ files.map((file) => ({ data: file.source, mediaType: file.mediaType })),
79
95
  )
80
96
 
81
97
  const selectedAgent = agents.find((item) => item.id === agentId)
@@ -143,16 +159,23 @@ export function ChatComposer({
143
159
  const clearedRevision = draftRevisionRef.current + 1
144
160
  draftRevisionRef.current = clearedRevision
145
161
  submittingRef.current = true
162
+ inFlightFilesRef.current = selectedFiles
146
163
  setText("")
147
164
  setFiles([])
148
165
  textareaRef.current?.focus()
149
166
  try {
150
167
  await onSubmit(value, selectedFiles)
168
+ revokeAgentChatComposerFiles(selectedFiles)
169
+ inFlightFilesRef.current = []
151
170
  } catch {
152
171
  if (draftRevisionRef.current === clearedRevision) {
153
172
  draftRevisionRef.current += 1
154
173
  setText(value)
155
174
  setFiles(selectedFiles)
175
+ inFlightFilesRef.current = []
176
+ } else {
177
+ revokeAgentChatComposerFiles(selectedFiles)
178
+ inFlightFilesRef.current = []
156
179
  }
157
180
  } finally {
158
181
  submittingRef.current = false
@@ -165,18 +188,19 @@ export function ChatComposer({
165
188
  data-agents24-chat-composer=""
166
189
  >
167
190
  {allowAttachments && files.length > 0 && (
168
- <div className="mb-2 flex min-w-0 gap-2 overflow-x-auto">
169
- {files.map((file, index) => (
170
- <div key={`${file.name}-${index}`} className="flex max-w-48 items-center gap-2 border bg-card px-2 py-1 text-xs">
171
- <span className="truncate">{file.name}</span>
172
- <Button variant="ghost" size="icon-xs" aria-label={`Remove ${file.name}`} onClick={() => {
173
- draftRevisionRef.current += 1
174
- setFiles((current) => current.filter((_, itemIndex) => itemIndex !== index))
175
- }}>
176
- <X />
177
- </Button>
178
- </div>
179
- ))}
191
+ <div className="mb-2 min-w-0">
192
+ <ChatAttachmentRows
193
+ attachments={files}
194
+ onRemove={(attachment) => {
195
+ if (!attachment.id) return
196
+ draftRevisionRef.current += 1
197
+ setFiles((current) => {
198
+ const removed = current.find((file) => file.id === attachment.id)
199
+ if (removed) revokeAgentChatComposerFiles([removed])
200
+ return current.filter((file) => file.id !== attachment.id)
201
+ })
202
+ }}
203
+ />
180
204
  </div>
181
205
  )}
182
206
  {attachmentError || recorder.error ? <p className="mb-2 text-xs text-destructive" role="alert">{attachmentError || recorder.error}</p> : null}
@@ -201,7 +225,7 @@ export function ChatComposer({
201
225
  attachmentMimeTypes.includes(canonicalInputMimeType(file.type))
202
226
  ))
203
227
  draftRevisionRef.current += 1
204
- setFiles((current) => [...current, ...accepted])
228
+ setFiles((current) => [...current, ...accepted.map(createAgentChatComposerFile)])
205
229
  setAttachmentError(
206
230
  accepted.length === selected.length
207
231
  ? null
@@ -1,7 +1,7 @@
1
1
  import type { ChatHitlPart, ChatMessage as ChatMessageModel } from "@agents24/react"
2
2
  import type { FeedbackReason, HitlResponse } from "@agents24/client"
3
3
  import type { AgentChatRuntimeHitlActionState } from "@agents24/chat-react/runtime"
4
- import { AgentChatDefaultActions, ChatAttachment, ChatAttachments } from "@agents24/chat-react/ui"
4
+ import { AgentChatDefaultActions, AgentChatUserMessageActionRow, ChatAttachmentRows, type ChatAttachmentContentResolver } from "@agents24/chat-react/ui"
5
5
  import * as React from "react"
6
6
 
7
7
  import { MessageParts } from "./message-parts"
@@ -20,6 +20,7 @@ type Props = {
20
20
  input: { rating: "like" | "dislike" | null; reason?: FeedbackReason | null; comment?: string | null },
21
21
  ) => Promise<void>
22
22
  Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
23
+ resolveAttachmentContent?: ChatAttachmentContentResolver
23
24
  }
24
25
 
25
26
  export function ChatMessage({
@@ -30,21 +31,19 @@ export function ChatMessage({
30
31
  onHitlAction,
31
32
  onRegenerate,
32
33
  onSetFeedback,
34
+ resolveAttachmentContent,
33
35
  Timeline,
34
36
  }: Props) {
35
37
  const isUser = message.role === "user"
36
38
  return (
37
39
  <Message align={isUser ? "end" : "start"}>
38
- <MessageContent>
40
+ <MessageContent className={isUser ? "gap-0" : undefined}>
39
41
  {isUser && message.attachments?.length ? (
40
- <ChatAttachments className="mb-2 justify-end">
41
- {message.attachments.map((attachment, index) => (
42
- <ChatAttachment
43
- data={attachment}
44
- key={String(attachment.id || attachment.url || attachment.filename || index)}
45
- />
46
- ))}
47
- </ChatAttachments>
42
+ <ChatAttachmentRows
43
+ attachments={message.attachments}
44
+ className="mb-2"
45
+ resolveContent={resolveAttachmentContent}
46
+ />
48
47
  ) : null}
49
48
  <Bubble variant={isUser ? "secondary" : "ghost"} align={isUser ? "end" : "start"} className={isUser ? "max-w-[min(75%,42rem)]" : "max-w-full overflow-visible"}>
50
49
  <BubbleContent className={isUser ? "px-4 py-2.5" : "w-full overflow-visible"}>
@@ -57,6 +56,16 @@ export function ChatMessage({
57
56
  />
58
57
  </BubbleContent>
59
58
  </Bubble>
59
+ {isUser && (
60
+ <AgentChatUserMessageActionRow>
61
+ <AgentChatDefaultActions
62
+ handlers={{
63
+ onCopy: (content) => { void navigator.clipboard.writeText(content).catch(() => undefined) },
64
+ }}
65
+ message={message}
66
+ />
67
+ </AgentChatUserMessageActionRow>
68
+ )}
60
69
  {!isUser && message.isFinal !== false && (
61
70
  <MessageFooter className="gap-1 opacity-70 transition-opacity group-hover/message:opacity-100">
62
71
  <AgentChatDefaultActions
@@ -100,11 +100,27 @@ export function ChatShell({
100
100
  || controller.runState === "reconnecting"
101
101
  || controller.runState === "paused"
102
102
  || controller.runState === "cancelling"
103
- || runtime.isSubmitting
103
+ const isUploading = controller.runState === "uploading" || runtime.isUploading
104
104
  const isCancelling = controller.runState === "cancelling"
105
105
  const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title
106
106
  const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)
107
107
  const isEmptyState = controller.activeThreadId === null && controller.messages.length === 0 && !hasBlockingError
108
+ const composerOverlayRef = React.useRef<HTMLDivElement>(null)
109
+ const [composerOverlayHeight, setComposerOverlayHeight] = React.useState(192)
110
+
111
+ React.useEffect(() => {
112
+ const overlay = composerOverlayRef.current
113
+ if (!overlay || isEmptyState) return
114
+
115
+ const updateHeight = () => {
116
+ setComposerOverlayHeight(Math.ceil(overlay.getBoundingClientRect().height))
117
+ }
118
+
119
+ updateHeight()
120
+ const resizeObserver = new ResizeObserver(updateHeight)
121
+ resizeObserver.observe(overlay)
122
+ return () => resizeObserver.disconnect()
123
+ }, [isEmptyState])
108
124
 
109
125
  return (
110
126
  <SidebarProvider
@@ -127,7 +143,7 @@ export function ChatShell({
127
143
  />}
128
144
  <SidebarInset className="h-svh min-h-0 overflow-hidden bg-background">
129
145
  <header
130
- className="flex h-12 shrink-0 items-center gap-2 border-b border-border/30 px-3 sm:px-4"
146
+ className="flex h-12 shrink-0 items-center gap-2 border-b border-border/50 px-3 sm:px-4"
131
147
  data-agents24-chat-header=""
132
148
  >
133
149
  <CollapsedHeaderActions onNew={runtime.newThread} />
@@ -151,7 +167,7 @@ export function ChatShell({
151
167
  <ChatComposer
152
168
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
153
169
  contextWindow={runtime.contextWindow}
154
- disabled={runtime.isBootstrapping || runtime.isChangingAgent}
170
+ disabled={runtime.isBootstrapping || runtime.isChangingAgent || isUploading}
155
171
  focusKey={controller.activeThreadId}
156
172
  forceExpanded
157
173
  isCancelling={isCancelling}
@@ -166,10 +182,11 @@ export function ChatShell({
166
182
  onSubmit={(text, files) => runtime.submit({
167
183
  text,
168
184
  files: files.map((file) => ({
169
- data: file,
170
- mediaType: file.type || "application/octet-stream",
171
- name: file.name,
185
+ data: file.source,
186
+ mediaType: file.mediaType,
187
+ name: file.filename,
172
188
  })),
189
+ attachments: files,
173
190
  })}
174
191
  />
175
192
  </div>
@@ -183,7 +200,8 @@ export function ChatShell({
183
200
  <MessageScroller className="relative min-h-0 flex-1">
184
201
  <MessageScrollerViewport preserveScrollOnPrepend>
185
202
  <MessageScrollerContent
186
- className="mx-auto w-full max-w-3xl gap-7 px-4 pt-8 pb-16 sm:px-6 sm:pt-12"
203
+ className="mx-auto w-full max-w-3xl gap-3 px-4 pt-8 pb-0 sm:px-6 sm:pt-12"
204
+ style={{ paddingBottom: composerOverlayHeight + 72 }}
187
205
  >
188
206
  {controller.hasOlderTurns && (
189
207
  <Button className="self-center" variant="ghost" size="sm" disabled={controller.isLoadingOlder} onClick={() => { void controller.loadOlder().catch(() => undefined) }}>
@@ -222,6 +240,7 @@ export function ChatShell({
222
240
  onHitlAction={runtime.onHitlAction}
223
241
  onRegenerate={() => controller.regenerate(message)}
224
242
  onSetFeedback={(input) => controller.setFeedback(message, input)}
243
+ resolveAttachmentContent={runtime.resolveAttachmentContent}
225
244
  Timeline={components?.Timeline}
226
245
  />
227
246
  </MessageScrollerItem>
@@ -229,35 +248,47 @@ export function ChatShell({
229
248
  </MessageScrollerContent>
230
249
  </MessageScrollerViewport>
231
250
  <TurnOutline messages={controller.messages} />
232
- <MessageScrollerButton />
233
251
  </MessageScroller>
252
+
253
+ <div
254
+ ref={composerOverlayRef}
255
+ className="pointer-events-none absolute inset-x-0 bottom-0 z-30 overflow-visible pt-10"
256
+ style={{
257
+ background: "linear-gradient(to bottom, transparent 0%, color-mix(in oklch, var(--background) 85%, transparent) 45%, var(--background) 65%, var(--background) 100%)",
258
+ }}
259
+ >
260
+ <div className="pointer-events-auto absolute inset-x-0 top-0 flex -translate-y-full justify-center pb-3">
261
+ <MessageScrollerButton className="!relative !inset-auto !translate-x-0 rtl:!translate-x-0" />
262
+ </div>
263
+ <div className="pointer-events-auto">
264
+ <ChatComposer
265
+ inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
266
+ contextWindow={runtime.contextWindow}
267
+ disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError || isUploading}
268
+ focusKey={controller.activeThreadId}
269
+ isCancelling={isCancelling}
270
+ isRunning={isRunning}
271
+ agentId={runtime.agentId}
272
+ agents={runtime.agents}
273
+ onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
274
+ onStop={() => { void controller.cancelRun().catch(() => undefined) }}
275
+ modelId={runtime.modelId}
276
+ models={runtime.models}
277
+ onModelChange={runtime.changeModel}
278
+ onSubmit={(text, files) => runtime.submit({
279
+ text,
280
+ files: files.map((file) => ({
281
+ data: file.source,
282
+ mediaType: file.mediaType,
283
+ name: file.filename,
284
+ })),
285
+ attachments: files,
286
+ })}
287
+ utilityRow="below"
288
+ />
289
+ </div>
290
+ </div>
234
291
  </MessageScrollerProvider>
235
- <div className="relative z-30 shrink-0 bg-background pt-3">
236
- <ChatComposer
237
- inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
238
- contextWindow={runtime.contextWindow}
239
- disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError}
240
- focusKey={controller.activeThreadId}
241
- isCancelling={isCancelling}
242
- isRunning={isRunning}
243
- agentId={runtime.agentId}
244
- agents={runtime.agents}
245
- onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
246
- onStop={() => { void controller.cancelRun().catch(() => undefined) }}
247
- modelId={runtime.modelId}
248
- models={runtime.models}
249
- onModelChange={runtime.changeModel}
250
- onSubmit={(text, files) => runtime.submit({
251
- text,
252
- files: files.map((file) => ({
253
- data: file,
254
- mediaType: file.type || "application/octet-stream",
255
- name: file.name,
256
- })),
257
- })}
258
- utilityRow="below"
259
- />
260
- </div>
261
292
  </>
262
293
  )}
263
294
  </section>
@@ -1,73 +1,31 @@
1
+ import { LatestThreadScroller, type LatestThreadScrollerOutlineItem } from "@agents24/chat-react"
1
2
  import type { ChatMessage } from "@agents24/react"
2
- import { useState } from "react"
3
3
 
4
- import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
5
- import { useMessageScroller, useMessageScrollerVisibility } from "../ui/message-scroller"
6
- import { cn } from "../../lib/utils"
7
-
8
- type Turn = { id: string; label: string; messageIds: string[]; response: string }
9
-
10
- function turnsFromMessages(messages: ChatMessage[]): Turn[] {
11
- return messages.reduce<Turn[]>((turns, message) => {
4
+ function turnsFromMessages(messages: ChatMessage[]): LatestThreadScrollerOutlineItem[] {
5
+ return messages.reduce<LatestThreadScrollerOutlineItem[]>((turns, message) => {
12
6
  if (message.role === "user") {
13
- turns.push({ id: message.id, label: message.content.trim() || `Turn ${turns.length + 1}`, messageIds: [message.id], response: "" })
7
+ turns.push({
8
+ id: message.id,
9
+ label: message.content.trim() || `Turn ${turns.length + 1}`,
10
+ messageIds: [message.id],
11
+ responsePreview: "",
12
+ })
14
13
  } else if (turns.length) {
15
14
  const turn = turns[turns.length - 1]
16
- turn.messageIds.push(message.id)
17
- if (!turn.response) turn.response = message.content.trim()
15
+ turn.messageIds?.push(message.id)
16
+ if (!turn.responsePreview) turn.responsePreview = message.content.trim()
18
17
  }
19
18
  return turns
20
19
  }, [])
21
20
  }
22
21
 
23
22
  export function TurnOutline({ messages }: { messages: ChatMessage[] }) {
24
- const turns = turnsFromMessages(messages)
25
- const { scrollToMessage } = useMessageScroller()
26
- const { currentAnchorId, visibleMessageIds } = useMessageScrollerVisibility()
27
- const visible = new Set(visibleMessageIds)
28
- const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
29
- const focusIndex = hoveredIndex ?? -1
30
- if (turns.length < 2) return null
31
-
32
23
  return (
33
- <nav aria-label="Transcript outline" className="pointer-events-none absolute end-2 top-1/2 z-20 hidden max-h-72 w-14 -translate-y-1/2 md:block xl:end-5">
34
- <div className="pointer-events-auto flex flex-col items-end gap-0.5 py-2">
35
- {turns.map((turn, index) => {
36
- const active = turn.messageIds.some((id) => visible.has(id) || currentAnchorId === id)
37
- const distance = focusIndex >= 0 ? Math.abs(index - focusIndex) : Number.POSITIVE_INFINITY
38
- const barWidth = focusIndex < 0
39
- ? 12
40
- : distance === 0 ? 50 : distance === 1 ? 36 : distance === 2 ? 23 : 10
41
- const barOpacity = focusIndex < 0
42
- ? active ? 1 : 0.4
43
- : distance === 0 ? 1 : distance === 1 ? 0.78 : distance === 2 ? 0.58 : 0.36
44
- return (
45
- <Tooltip key={turn.id}>
46
- <TooltipTrigger asChild>
47
- <button
48
- type="button"
49
- aria-label={`Jump to ${turn.label}`}
50
- className="group relative flex h-2 w-14 items-center justify-end outline-none after:pointer-events-auto after:absolute after:-inset-y-0.5 after:inset-x-0 after:content-['']"
51
- onBlur={() => setHoveredIndex(null)}
52
- onFocus={() => setHoveredIndex(index)}
53
- onMouseEnter={() => setHoveredIndex(index)}
54
- onMouseLeave={() => setHoveredIndex(null)}
55
- onClick={() => scrollToMessage(turn.id, { align: "start", behavior: "smooth" })}
56
- >
57
- <span
58
- className={cn("h-0.5 rounded-full bg-muted-foreground/40 transition-[width,background-color,opacity] duration-150 ease-out group-hover:bg-foreground group-focus-visible:bg-foreground", active && "bg-foreground")}
59
- style={{ opacity: barOpacity, width: barWidth }}
60
- />
61
- </button>
62
- </TooltipTrigger>
63
- <TooltipContent side="left" className="w-64 flex-col items-stretch gap-0 overflow-hidden p-3">
64
- <p className="line-clamp-2 max-w-full wrap-break-word text-sm font-medium">{turn.label}</p>
65
- {turn.response && <p className="mt-1 line-clamp-2 max-w-full wrap-break-word text-xs opacity-70">{turn.response}</p>}
66
- </TooltipContent>
67
- </Tooltip>
68
- )
69
- })}
70
- </div>
71
- </nav>
24
+ <LatestThreadScroller.Outline
25
+ items={turnsFromMessages(messages)}
26
+ minSideGutterForRail={0}
27
+ side="left"
28
+ verticalOffset="3.5rem"
29
+ />
72
30
  )
73
31
  }
@@ -55,7 +55,7 @@ function MessageScrollerContent({
55
55
  return (
56
56
  <MessageScrollerPrimitive.Content
57
57
  data-slot="message-scroller-content"
58
- className={cn("flex h-max min-h-full flex-col gap-8", className)}
58
+ className={cn("flex h-max min-h-full flex-col gap-3", className)}
59
59
  {...props}
60
60
  />
61
61
  )
@@ -74,7 +74,7 @@ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
74
74
  <div
75
75
  data-slot="message-footer"
76
76
  className={cn(
77
- "flex max-w-full min-w-0 items-center px-3.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
77
+ "flex max-w-full min-w-0 items-center pl-3.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
78
78
  className
79
79
  )}
80
80
  {...props}
@@ -215,7 +215,7 @@ function Sidebar({
215
215
  // Adjust the padding for floating and inset variants.
216
216
  variant === "floating" || variant === "inset"
217
217
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
218
- : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
218
+ : "border-border/50 group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
219
219
  className
220
220
  )}
221
221
  {...props}