diff --git a/apps/agentic-chat/src/components/AuroraBackground.tsx b/apps/agentic-chat/src/components/AuroraBackground.tsx
index ba72985f..f3da6d00 100644
--- a/apps/agentic-chat/src/components/AuroraBackground.tsx
+++ b/apps/agentic-chat/src/components/AuroraBackground.tsx
@@ -21,12 +21,18 @@ function isWebGLAvailable(): boolean {
}
function CSSFallback() {
+ // Approximates the WebGL Aurora's purple-to-green palette (colorA #7B2FBE,
+ // colorB #00CD98, colorC #A855F7) with layered radial gradients.
return (
)
@@ -77,7 +83,26 @@ function AuroraCanvas({ onError }: { onError: () => void }) {
shader.destroy()
return
}
- cleanup = () => shader.destroy()
+
+ // createShader pins the canvas to a fixed pixel size and watches the
+ // canvas itself for resizes — so CSS-driven layout changes (e.g. the
+ // sidebar opening/closing) never reach it. Observe the parent instead
+ // and resize explicitly.
+ const parent = canvas.parentElement
+ let resizeObserver: ResizeObserver | null = null
+ if (parent) {
+ resizeObserver = new ResizeObserver(([entry]) => {
+ if (!entry) return
+ const { width, height } = entry.contentRect
+ if (width > 0 && height > 0) shader.resize(width, height)
+ })
+ resizeObserver.observe(parent)
+ }
+
+ cleanup = () => {
+ resizeObserver?.disconnect()
+ shader.destroy()
+ }
})
.catch(err => {
console.error('[AuroraBackground] shader init failed, falling back to CSS:', err)
diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx
index 6de5a268..c03e5d00 100644
--- a/apps/agentic-chat/src/components/Chat.tsx
+++ b/apps/agentic-chat/src/components/Chat.tsx
@@ -9,12 +9,14 @@ import { AssistantMessage } from './AssistantMessage'
import { AuroraBackground } from './AuroraBackground'
import { Composer } from './Composer'
import { LoadingIndicator } from './LoadingIndicator'
-import { Button } from './ui/Button'
+import { PopularActionsCarousel } from './PopularActionsCarousel'
import { UserMessage } from './UserMessage'
-const WELCOME_SUGGESTIONS = [
+const POPULAR_ACTIONS = [
+ 'Show my recent transaction activity',
+ 'Create a stop loss for my ETH position',
+ 'Swap half my USDC on Ethereum to FOX',
'What is my USDC balance on Arbitrum?',
- 'Swap half my USDC on arb to FOX',
'Give me some info about FOX on Arb',
]
@@ -117,23 +119,7 @@ export function Chat() {
{/* Suggestions above composer - only shown when empty */}
- {isEmpty && (
-
-
- {WELCOME_SUGGESTIONS.map((suggestion, index) => (
-
- ))}
-
-
- )}
+ {isEmpty && }
{/* Composer */}
diff --git a/apps/agentic-chat/src/components/PopularActionsCarousel.tsx b/apps/agentic-chat/src/components/PopularActionsCarousel.tsx
new file mode 100644
index 00000000..4ffe82c5
--- /dev/null
+++ b/apps/agentic-chat/src/components/PopularActionsCarousel.tsx
@@ -0,0 +1,134 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+
+import { Button } from './ui/Button'
+
+type PopularActionsCarouselProps = {
+ actions: string[]
+ onActionClick: (action: string) => void
+}
+
+const AUTO_ADVANCE_MS = 5000
+
+export function PopularActionsCarousel({ actions, onActionClick }: PopularActionsCarouselProps) {
+ const containerRef = useRef
(null)
+ const [activeIndex, setActiveIndex] = useState(0)
+ const [isPaused, setIsPaused] = useState(false)
+ const pauseTimeoutRef = useRef | null>(null)
+ const rafRef = useRef(null)
+
+ const stopResumeTimer = useCallback(() => {
+ if (!pauseTimeoutRef.current) return
+ clearTimeout(pauseTimeoutRef.current)
+ pauseTimeoutRef.current = null
+ }, [])
+
+ const resumeAfterInteraction = useCallback(() => {
+ stopResumeTimer()
+ pauseTimeoutRef.current = setTimeout(() => {
+ setIsPaused(false)
+ pauseTimeoutRef.current = null
+ }, 1200)
+ }, [stopResumeTimer])
+
+ const goToSlide = useCallback((index: number) => {
+ const container = containerRef.current
+ if (!container) return
+ const item = container.querySelector(`[data-action-index="${index}"]`)
+ if (!item) return
+ item.scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' })
+ setActiveIndex(index)
+ }, [])
+
+ const handleScroll = useCallback(() => {
+ if (rafRef.current) cancelAnimationFrame(rafRef.current)
+
+ rafRef.current = requestAnimationFrame(() => {
+ const container = containerRef.current
+ if (!container) return
+
+ const children = Array.from(container.querySelectorAll('[data-action-index]'))
+ if (children.length === 0) return
+
+ let closestIndex = 0
+ let closestDistance = Number.POSITIVE_INFINITY
+
+ children.forEach((child, index) => {
+ const distance = Math.abs(child.offsetLeft - container.scrollLeft)
+ if (distance < closestDistance) {
+ closestDistance = distance
+ closestIndex = index
+ }
+ })
+
+ setActiveIndex(closestIndex)
+ })
+ }, [])
+
+ useEffect(() => {
+ if (actions.length <= 1 || isPaused) return
+
+ const timer = setInterval(() => {
+ const nextIndex = (activeIndex + 1) % actions.length
+ goToSlide(nextIndex)
+ }, AUTO_ADVANCE_MS)
+
+ return () => clearInterval(timer)
+ }, [actions.length, activeIndex, goToSlide, isPaused])
+
+ useEffect(() => {
+ return () => {
+ stopResumeTimer()
+ if (rafRef.current) cancelAnimationFrame(rafRef.current)
+ }
+ }, [stopResumeTimer])
+
+ return (
+ setIsPaused(true)}
+ onMouseLeave={() => setIsPaused(false)}
+ onFocusCapture={() => setIsPaused(true)}
+ onBlurCapture={() => setIsPaused(false)}
+ >
+
+
{
+ stopResumeTimer()
+ setIsPaused(true)
+ }}
+ onTouchEnd={resumeAfterInteraction}
+ onTouchCancel={resumeAfterInteraction}
+ role="region"
+ aria-label="Popular actions"
+ >
+ {actions.map((action, index) => (
+
+ ))}
+
+
+ {actions.map((action, index) => (
+
+
+
+ )
+}