feat(web): redesign Slack page Featured Workflow section with YouTube… (#10880)

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
MP 2026-01-21 19:47:42 -08:00 committed by GitHub
parent 2305888746
commit d87abe8efc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 633 additions and 68 deletions

View file

@ -5,7 +5,6 @@ import {
GitBranch,
GraduationCap,
Link2,
LucideIcon,
MessageSquare,
Settings,
Shield,
@ -13,13 +12,15 @@ import {
Users,
Zap,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import type { Metadata } from "next"
import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
import { Button } from "@/components/ui"
import { EXTERNAL_LINKS } from "@/lib/constants"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
const TITLE = "Roo Code for Slack"
const DESCRIPTION =
@ -69,7 +70,7 @@ export const metadata: Metadata = {
// Invalidate cache when a request comes in, at most once every hour.
export const revalidate = 3600
interface ValueProp {
type ValueProp = {
icon: LucideIcon
title: string
description: string
@ -112,11 +113,10 @@ const VALUE_PROPS: ValueProp[] = [
},
]
interface WorkflowStep {
type WorkflowStep = {
step: number
title: string
description: string
code?: string
}
const WORKFLOW_STEPS: WorkflowStep[] = [
@ -124,7 +124,6 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
step: 1,
title: "Turn the discussion into a plan",
description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
code: "@Roomote plan out a dark mode feature based on our discussion. Include the toggle, persistence, and system preference detection.",
},
{
step: 2,
@ -136,7 +135,6 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
step: 3,
title: "Build the plan",
description: "Once the plan looks good, hand it off to the Coder agent to implement.",
code: "@Roomote implement this plan in the frontend-web repo.",
},
{
step: 4,
@ -145,7 +143,7 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
},
]
interface OnboardingStep {
type OnboardingStep = {
icon: LucideIcon
title: string
description: string
@ -182,48 +180,54 @@ const ONBOARDING_STEPS: OnboardingStep[] = [
},
]
export default function SlackPage() {
export default function SlackPage(): JSX.Element {
return (
<>
{/* Hero Section */}
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
<AnimatedBackground />
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-4xl mx-auto mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
<Slack className="size-4" />
Powered by Roo Code Cloud
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
<Slack className="size-4" />
Powered by Roo Code Cloud
</div>
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
<span className="text-violet-500">@Roomote:</span> Your AI Team in&nbsp;Slack
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
without leaving the conversation.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<Button
size="xl"
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Get Started
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
<a
href={EXTERNAL_LINKS.SLACK_DOCS}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Read the Docs
</a>
</Button>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
<span className="text-violet-500">@Roomote:</span> Your AI Team in&nbsp;Slack
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without
leaving the conversation.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="xl"
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Get Started
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
<a
href={EXTERNAL_LINKS.SLACK_DOCS}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Read the Docs
</a>
</Button>
<div className="flex justify-center lg:justify-end">
<SlackThreadDemo />
</div>
</div>
</div>
@ -264,13 +268,13 @@ export default function SlackPage() {
</section>
{/* Featured Workflow Section */}
<section className="relative overflow-hidden border-t border-border py-32">
<section className="relative overflow-hidden border-t border-border py-24 lg:py-32">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
</div>
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
<div className="mx-auto mb-12 max-w-5xl text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
<Zap className="size-4" />
Featured Workflow
@ -283,29 +287,42 @@ export default function SlackPage() {
</p>
</div>
<div className="relative mx-auto md:max-w-[1000px]">
{/* Workflow Steps */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{WORKFLOW_STEPS.map((step) => (
<div
key={step.step}
className="relative border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
<div className="flex items-center gap-3 mb-4">
<div className="bg-blue-100 dark:bg-blue-900/30 w-10 h-10 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold">
{step.step}
<div className="relative mx-auto max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
{/* YouTube Video Embed */}
<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
<iframe
className="aspect-video w-full"
src="https://www.youtube-nocookie.com/embed/dJM_8HHGe1E?rel=0"
title="Roo Code Slack Integration Demo"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
</div>
{/* Workflow Steps */}
<div className="lg:col-span-2 space-y-3">
{WORKFLOW_STEPS.map((step) => (
<div
key={step.step}
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
<div className="flex items-start gap-3">
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
{step.step}
</div>
<div className="min-w-0">
<h3 className="text-base font-semibold text-foreground mb-0.5">
{step.title}
</h3>
<p className="text-sm leading-snug text-muted-foreground">
{step.description}
</p>
</div>
</div>
<h3 className="text-xl font-semibold text-foreground">{step.title}</h3>
</div>
<p className="leading-relaxed font-light text-muted-foreground mb-4">
{step.description}
</p>
{step.code && (
<div className="bg-muted/50 rounded-lg p-4 font-mono text-sm text-foreground/80 border border-border/50">
{step.code}
</div>
)}
</div>
))}
))}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,548 @@
"use client"
import type { ReactNode } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { CheckCircle2, Paperclip } from "lucide-react"
import { cn } from "@/lib/utils"
type SlackMessage = {
id: string
author: string
timeLabel: string
body: ReactNode
avatarText: string
avatarClassName: string
kind: "human" | "bot"
}
function usePrefersReducedMotion(): boolean {
const [reduced, setReduced] = useState(false)
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
const onChange = () => setReduced(media.matches)
onChange()
if (typeof media.addEventListener === "function") {
media.addEventListener("change", onChange)
return () => media.removeEventListener("change", onChange)
}
media.addListener?.(onChange)
return () => media.removeListener?.(onChange)
}, [])
return reduced
}
type TypingDotsProps = {
className?: string
}
function TypingDots({ className }: TypingDotsProps): JSX.Element {
return (
<span className={cn("inline-flex items-center gap-1", className)} aria-hidden="true">
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
</span>
)
}
type FakeLinkProps = {
children: ReactNode
className?: string
}
function FakeLink({ children, className }: FakeLinkProps): JSX.Element {
return (
<span className={cn("text-violet-300 underline underline-offset-2", "cursor-default", className)}>
{children}
</span>
)
}
type SlackMessageRowProps = {
message: SlackMessage
isNew: boolean
reduceMotion: boolean
}
function SlackMessageRow({ message, isNew, reduceMotion }: SlackMessageRowProps): JSX.Element {
let animation = ""
if (!reduceMotion && isNew) {
animation = "animate-in fade-in slide-in-from-bottom-2 duration-500"
}
return (
<div className={cn("flex gap-3", animation)}>
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
message.avatarClassName,
)}>
{message.avatarText}
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span className="text-[13px] font-semibold text-[#F8F8F9]">{message.author}</span>
<span className="text-[11px] text-[#8B8D91]">{message.timeLabel}</span>
{message.kind === "bot" && (
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
<CheckCircle2 className="h-3 w-3" />
App
</span>
)}
</div>
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{message.body}</div>
</div>
</div>
)
}
export type SlackThreadDemoProps = {
className?: string
}
export function SlackThreadDemo({ className }: SlackThreadDemoProps): JSX.Element {
const reduceMotion = usePrefersReducedMotion()
const [stepIndex, setStepIndex] = useState(0)
const scrollViewportRef = useRef<HTMLDivElement>(null)
const messages: SlackMessage[] = useMemo(
() => [
{
id: "m1",
author: "Avery Lee",
timeLabel: "Monday at 2:56 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<span>We need to add a page to our Marketing site that highlights using Roo Code from Slack.</span>
),
},
{
id: "m2",
author: "Avery Lee",
timeLabel: "Monday at 2:58 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-y-2">
<div>
The documentation for using Roo Code from Slack is here:{" "}
<FakeLink className="hover:text-violet-200">
https://docs.roocode.com/roo-code-cloud/slack-integration
</FakeLink>
</div>
<div className="text-[#B8BBC0]">Here are some pages from our site we can use for guidance:</div>
<ol className="list-decimal pl-5 text-[#D1D2D3]">
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com</FakeLink>
</li>
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com/extension</FakeLink>
</li>
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com/cloud</FakeLink>
</li>
</ol>
</div>
),
},
{
id: "m3",
author: "Avery Lee",
timeLabel: "Monday at 3:08 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-y-3">
<div>This is the start of a wireframe I have in mind for this page</div>
<div className="w-full max-w-[420px] rounded-lg border border-white/10 bg-black/20 p-3">
<div className="flex items-center gap-2 text-[12px] text-[#B8BBC0]">
<Paperclip className="h-4 w-4" />
IMG_9721.heic
</div>
<div className="mt-3 h-24 w-full rounded-md bg-gradient-to-br from-white/10 via-white/5 to-white/0" />
</div>
</div>
),
},
{
id: "m4",
author: "Avery Lee",
timeLabel: "Monday at 3:09 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-x-2">
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink>
<span>let&apos;s create the plan to deliver this</span>
</div>
),
},
{
id: "m5",
author: "Roomote (Roo Code)",
timeLabel: "Monday at 3:09 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[#D1D2D3]">
Calling <span className="font-semibold text-[#F8F8F9]">Planneroo</span> to get started on
your task on{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
RooCodeInc/Roo-Code
</code>
</div>
<div>
<button
type="button"
className="inline-flex items-center rounded-md border border-white/10 bg-transparent px-2 py-1 text-[12px] font-medium text-[#D1D2D3] hover:bg-white/5">
Cancel
</button>
</div>
</div>
),
},
{
id: "m6",
author: "Roomote (Roo Code)",
timeLabel: "Monday at 3:10 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-x-2">
<span>Cool, I&apos;ll knock this out real quick.</span>
<FakeLink className="hover:text-violet-200">Follow along</FakeLink>
</div>
),
},
{
id: "m7",
author: "Roomote (Roo Code)",
timeLabel: "Monday at 3:12 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-2">
<div className="font-semibold text-[#F8F8F9]">Todo List:</div>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<ul className="space-y-1">
{[
"Analyze existing page structures and component patterns",
"Review marketing content requirements and wireframe details",
"Create detailed component architecture plan",
"Design page structure and section breakdown",
"Plan navigation updates and integration points",
"Test the page and verify all sections work",
].map((item) => (
<li key={item} className="text-[#D1D2D3]">
<span className="mr-2"></span>
<span className="line-through opacity-80">{item}</span>
</li>
))}
</ul>
</div>
<div className="text-[12px] text-[#8B8D91]">(edited)</div>
</div>
),
},
{
id: "m8",
author: "Roomote (Roo Code)",
timeLabel: "Monday at 3:16 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<p>
I&apos;ve created a comprehensive implementation plan for the Roo Code Slack integration
marketing page at{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
plans/slack-marketing-page-plan.md
</code>
.
</p>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Plan Overview</div>
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
<li>
<span className="mr-2"></span>Hero + dual CTAs
</li>
<li>
<span className="mr-2"></span>Value props grid
</li>
<li>
<span className="mr-2"></span>Thread to Shipped Feature workflow
</li>
<li>
<span className="mr-2"></span>Onboarding steps + CTA
</li>
</ul>
</div>
<div className="space-x-2">
<span className="text-[#B8BBC0]">Full document:</span>
<FakeLink className="hover:text-violet-200">View artifact</FakeLink>
</div>
<div className="text-[12px] italic text-[#8B8D91]">
Want to follow up? Just @-mention me in your response.
</div>
</div>
),
},
{
id: "m9",
author: "Avery Lee",
timeLabel: "Monday at 3:17 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-x-2">
<FakeLink className="no-underline hover:text-violet-200">@Roomote (Roo Code)</FakeLink>
<span>this looks great, let&apos;s use Coderoo to build this</span>
</div>
),
},
{
id: "m10",
author: "Roomote (Roo Code)",
timeLabel: "Monday at 3:23 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<p>
I&apos;ve built the Roo Code Slack integration marketing page. Here&apos;s what was
implemented:
</p>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Files</div>
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/app/slack/page.tsx
</code>{" "}
Slack marketing page
</li>
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/lib/constants.ts
</code>{" "}
added{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
SLACK_DOCS
</code>
</li>
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/components/chromes/nav-bar.tsx
</code>{" "}
added Slack to Product dropdown
</li>
</ul>
</div>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Pull Request</div>
<div className="mt-2">
<span className="font-semibold text-[#F8F8F9]">PR #10853</span>:{" "}
<FakeLink className="hover:text-violet-200">
https://github.com/RooCodeInc/Roo-Code/pull/10853
</FakeLink>
</div>
</div>
<p className="text-[#B8BBC0]">
The page is accessible at{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">/slack</code>{" "}
and includes navigation links in desktop and mobile.
</p>
</div>
),
},
],
[],
)
type DemoPhase =
| { kind: "show"; messageIndex: number }
| { kind: "typing"; messageIndex: number }
| { kind: "reset" }
const phases: DemoPhase[] = useMemo(() => {
const next: DemoPhase[] = []
if (messages.length === 0) return [{ kind: "reset" }]
next.push({ kind: "typing", messageIndex: 0 })
next.push({ kind: "show", messageIndex: 0 })
for (let messageIndex = 1; messageIndex < messages.length; messageIndex += 1) {
next.push({ kind: "typing", messageIndex })
next.push({ kind: "show", messageIndex })
}
next.push({ kind: "reset" })
return next
}, [messages])
const lastShowPhaseIndex = useMemo(() => {
let lastIndex = -1
for (let idx = 0; idx < phases.length; idx += 1) {
if (phases[idx]?.kind === "show") lastIndex = idx
}
return lastIndex
}, [phases])
useEffect(() => {
if (reduceMotion) {
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
return
}
const active = phases[stepIndex] ?? phases.at(0)
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
const durationMs = (() => {
const base = 2200
if (active?.kind === "reset") return 500
if (active?.kind === "typing") return 900
return isLastMessageShow ? base * 2 : base
})()
const timer = window.setTimeout(() => {
setStepIndex((prev) => (prev + 1) % phases.length)
}, durationMs)
return () => window.clearTimeout(timer)
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "reset" }
function getVisibleCount(phase: DemoPhase): number {
if (phase.kind === "reset") return 0
if (phase.kind === "typing") return phase.messageIndex
return phase.messageIndex + 1
}
const visibleCount = getVisibleCount(activePhase)
const visibleMessages = messages.slice(0, visibleCount)
const typingTarget = activePhase.kind === "typing" ? messages[activePhase.messageIndex] : undefined
useEffect(() => {
const viewport = scrollViewportRef.current
if (!viewport) return
if (activePhase.kind === "reset" || visibleCount <= 1) {
viewport.scrollTo({ top: 0, behavior: "auto" })
return
}
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: reduceMotion ? "auto" : "smooth",
})
}, [activePhase.kind, reduceMotion, visibleCount])
return (
<div
className={cn("w-full max-w-[620px] h-[520px] sm:h-[560px]", className)}
role="img"
aria-label="Animated Slack thread showing Roo Code responding as @Roomote">
<div
aria-hidden="true"
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1A1D21] shadow-2xl shadow-black/30">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
<div className="flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-[#F24A4A]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#F2C94C]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#27AE60]" />
<div className="ml-3 text-sm font-semibold text-[#F8F8F9]">Thread</div>
</div>
<div className="flex items-center gap-2 text-[11px] text-[#8B8D91]">
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
Live demo
</div>
</div>
<div
ref={scrollViewportRef}
className="flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.18)_transparent]">
<div
className={cn(
"space-y-5 transition-opacity duration-300 will-change-opacity",
activePhase.kind === "reset" ? "opacity-0" : "opacity-100",
)}>
{visibleMessages.map((message) => (
<SlackMessageRow
key={message.id}
message={message}
reduceMotion={reduceMotion}
isNew={
activePhase.kind === "show" && messages[activePhase.messageIndex]?.id === message.id
}
/>
))}
{typingTarget && (
<div className={cn(reduceMotion ? "" : "animate-in fade-in duration-300", "flex gap-3")}>
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
typingTarget.avatarClassName,
)}>
{typingTarget.avatarText}
</div>
<div className="min-w-0">
<div className="flex items-baseline gap-x-2">
<span className="text-[13px] font-semibold text-[#F8F8F9]">
{typingTarget.author}
</span>
{typingTarget.kind === "bot" && (
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
<CheckCircle2 className="h-3 w-3" />
App
</span>
)}
<span className="text-[11px] text-[#8B8D91]">typing</span>
</div>
<div className="mt-2">
<TypingDots />
</div>
</div>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between border-t border-white/10 px-4 py-3">
<div className="flex items-center gap-1.5">
{messages.map((message, idx) => (
<span
key={message.id}
className={cn(
"h-1.5 w-5 rounded-full transition-colors duration-300",
Math.max(0, visibleCount - 1) === idx ? "bg-violet-300" : "bg-white/10",
)}
/>
))}
</div>
</div>
</div>
</div>
)
}

View file

@ -74,7 +74,7 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
afterAll(async () => {
await fs.rm(tmpDir, { recursive: true, force: true })
})
}, 60_000) // 60 second timeout for Windows cleanup
describe(`${klass.name}#getDiff`, () => {
it("returns the correct diff between commits", async () => {