From 1390e311f0fb5d1793d4261300c9edf3bee0fb2c Mon Sep 17 00:00:00 2001 From: Tim Black Date: Wed, 6 May 2026 11:59:31 -0700 Subject: [PATCH 01/10] chore: gitignore Nx cache and workspace-data Co-authored-by: Cursor --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4684df71..50726f64 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,7 @@ Thumbs.db # Playwright MCP .playwright-mcp + +# Nx — local machine cache (agents/CI should never commit these) +.nx/cache +.nx/workspace-data From 2987fc9706857c8d07322e5d2d803e2c65ad36d8 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Tue, 12 May 2026 21:33:51 -0700 Subject: [PATCH 02/10] adding rounder feels --- apps/agentic-chat/src/components/Composer.tsx | 2 +- apps/agentic-chat/src/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/agentic-chat/src/components/Composer.tsx b/apps/agentic-chat/src/components/Composer.tsx index 1b9988b6..5301e787 100644 --- a/apps/agentic-chat/src/components/Composer.tsx +++ b/apps/agentic-chat/src/components/Composer.tsx @@ -55,7 +55,7 @@ export function Composer() { onKeyDown={onKeyDown} placeholder="Write a message..." rows={1} - className="flex-1 resize-none rounded-lg border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + className="flex-1 resize-none rounded-2xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring" style={ { minHeight: '48px', diff --git a/apps/agentic-chat/src/styles.css b/apps/agentic-chat/src/styles.css index 56e15b26..341611f4 100644 --- a/apps/agentic-chat/src/styles.css +++ b/apps/agentic-chat/src/styles.css @@ -72,7 +72,7 @@ } :root { - --radius: 0.5rem; + --radius: 0.75rem; --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); --card: oklch(1 0 0); From 991d8fa0149f6c637888264b7b8bb5b6b003505b Mon Sep 17 00:00:00 2001 From: Tim Black Date: Mon, 27 Apr 2026 13:48:05 -0700 Subject: [PATCH 03/10] feat(SS-5656): aurora shader background on empty chat state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses shaders.com instead of react-three/fiber — no GLSL, no render loop, WebGPU-first with WebGL2 fallback built in. ShapeShift brand palette (purple-500 / green-500 / purple-300) with slow drift. Gated behind useIsMobile (768px) and a WebGL capability check; falls back to a CSS radial-gradient on mobile or when WebGL is unavailable. Shader chunk is lazy-loaded so it doesn't hit the main bundle. Co-Authored-By: Claude Sonnet 4.6 --- apps/agentic-chat/package.json | 1 + .../src/components/AuroraBackground.tsx | 62 +++++++++++++++++++ apps/agentic-chat/src/components/Chat.tsx | 6 +- bun.lock | 5 ++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 apps/agentic-chat/src/components/AuroraBackground.tsx diff --git a/apps/agentic-chat/package.json b/apps/agentic-chat/package.json index 90dd628b..01051bcb 100644 --- a/apps/agentic-chat/package.json +++ b/apps/agentic-chat/package.json @@ -55,6 +55,7 @@ "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", + "shaders": "^2.5.109", "sonner": "^2.0.7", "tailwind-merge": "^3.2.0", "viem": "*", diff --git a/apps/agentic-chat/src/components/AuroraBackground.tsx b/apps/agentic-chat/src/components/AuroraBackground.tsx new file mode 100644 index 00000000..2379f201 --- /dev/null +++ b/apps/agentic-chat/src/components/AuroraBackground.tsx @@ -0,0 +1,62 @@ +import { lazy, Suspense } from 'react' + +import { useIsMobile } from '@/hooks/use-mobile' + +const ShaderAurora = lazy(() => + import('shaders/react').then(m => ({ + default: function AuroraShader() { + const { Shader, Aurora } = m + return ( + + + + ) + }, + })) +) + +function isWebGLAvailable(): boolean { + try { + const canvas = document.createElement('canvas') + return !!(window.WebGLRenderingContext && (canvas.getContext('webgl2') ?? canvas.getContext('webgl'))) + } catch { + return false + } +} + +function CSSFallback() { + return ( +
+ ) +} + +export function AuroraBackground() { + const isMobile = useIsMobile() + + if (isMobile || !isWebGLAvailable()) { + return + } + + return ( + }> + + + ) +} diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index 5c1e1e75..db4f8c53 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -6,6 +6,7 @@ import { useStreamPauseDetector } from '../hooks/useStreamPauseDetector' import { useChatContext } from '../providers/ChatProvider' import { AssistantMessage } from './AssistantMessage' +import { AuroraBackground } from './AuroraBackground' import { Composer } from './Composer' import { LoadingIndicator } from './LoadingIndicator' import { Button } from './ui/Button' @@ -93,8 +94,9 @@ export function Chat() { {/* Messages viewport */}
{isEmpty ? ( -
-
How can I help you today?
+
+ +
How can I help you today?
) : ( Date: Mon, 27 Apr 2026 15:16:00 -0700 Subject: [PATCH 04/10] fix(SS-5656): start aurora animation loop + glass chrome on header/bottom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Animation fix: shaders.com IntersectionObserver only calls startAnimation() on a not-visible → visible transition. Mounting with display:none then flipping to block on the next animation frame forces that transition. UI: header, suggestion strip, and composer get bg/80 backdrop-blur-md so the aurora bleeds through. Header and suggestion strip get a border to ground them visually against the background. Built with Claude --- apps/agentic-chat/src/app/dashboard/page.tsx | 2 +- .../src/components/AuroraBackground.tsx | 42 ++++++++++++------- apps/agentic-chat/src/components/Chat.tsx | 4 +- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/agentic-chat/src/app/dashboard/page.tsx b/apps/agentic-chat/src/app/dashboard/page.tsx index db3e1a0e..d2886e3e 100644 --- a/apps/agentic-chat/src/app/dashboard/page.tsx +++ b/apps/agentic-chat/src/app/dashboard/page.tsx @@ -13,7 +13,7 @@ export const Dashboard = () => { {isSidebarLeftEnabled && } -
+
{isSidebarLeftEnabled && }
diff --git a/apps/agentic-chat/src/components/AuroraBackground.tsx b/apps/agentic-chat/src/components/AuroraBackground.tsx index 2379f201..2acae802 100644 --- a/apps/agentic-chat/src/components/AuroraBackground.tsx +++ b/apps/agentic-chat/src/components/AuroraBackground.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from 'react' +import { lazy, Suspense, useEffect, useRef, useState } from 'react' import { useIsMobile } from '@/hooks/use-mobile' @@ -6,21 +6,33 @@ const ShaderAurora = lazy(() => import('shaders/react').then(m => ({ default: function AuroraShader() { const { Shader, Aurora } = m + // Start hidden so IntersectionObserver sees the transition to visible, + // which is what triggers the animation loop in the shaders library. + const [visible, setVisible] = useState(false) + const rafRef = useRef(0) + + useEffect(() => { + rafRef.current = requestAnimationFrame(() => setVisible(true)) + return () => cancelAnimationFrame(rafRef.current) + }, []) + return ( - - - +
+ + + +
) }, })) diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index db4f8c53..6de5a268 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -118,7 +118,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) => ( +
+
+
+ ) +} From 18d3ab4d328073bec0ceefb8d1cc9b951cc04d85 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Thu, 21 May 2026 10:42:02 -0700 Subject: [PATCH 09/10] feat: reorder popular actions for clearer carousel ordering Built with Claude --- apps/agentic-chat/src/components/Chat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index fa18ee2c..c03e5d00 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -13,11 +13,11 @@ import { PopularActionsCarousel } from './PopularActionsCarousel' import { UserMessage } from './UserMessage' const POPULAR_ACTIONS = [ - 'What is my USDC balance on Arbitrum?', - 'Swap half my USDC on arb to FOX', - 'Give me some info about FOX on Arb', '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?', + 'Give me some info about FOX on Arb', ] export function Chat() { From 71fd57b21e2fadfb8a69ce7694de7e7a04624af0 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Thu, 21 May 2026 11:11:07 -0700 Subject: [PATCH 10/10] fix: aurora background resize and mobile fallback Observe the canvas's parent container and call shader.resize() explicitly so the WebGL aurora re-renders to full size after layout changes (e.g. the sidebar opening/closing). The shader library pins the canvas to fixed pixels and watches the frozen canvas for resizes, so CSS-driven changes never reached it. Also make the mobile CSSFallback more vibrant with layered radial gradients matching the Aurora palette, instead of a faint two-layer wash. Built with Claude --- .../src/components/AuroraBackground.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) 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)