diff --git a/apps/web-roo-code/src/app/linear/page.tsx b/apps/web-roo-code/src/app/linear/page.tsx
new file mode 100644
index 00000000000..40334e2698a
--- /dev/null
+++ b/apps/web-roo-code/src/app/linear/page.tsx
@@ -0,0 +1,413 @@
+import {
+ ArrowRight,
+ CheckCircle,
+ CreditCard,
+ Eye,
+ GitBranch,
+ GitPullRequest,
+ Link2,
+ MessageSquare,
+ Settings,
+ Shield,
+} from "lucide-react"
+import type { LucideIcon } from "lucide-react"
+import type { Metadata } from "next"
+
+import { AnimatedBackground } from "@/components/homepage"
+import { LinearIssueDemo } from "@/components/linear/linear-issue-demo"
+import { Button } from "@/components/ui"
+import { EXTERNAL_LINKS } from "@/lib/constants"
+import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
+
+const TITLE = "Roo Code for Linear"
+const DESCRIPTION = "Assign development work to @Roo Code directly from Linear. Get PRs back without switching tools."
+const OG_DESCRIPTION = "Turn Linear Issues into Pull Requests"
+const PATH = "/linear"
+
+// Featured Workflow section is temporarily commented out until video is ready
+// const LINEAR_DEMO_YOUTUBE_ID = ""
+
+export const metadata: Metadata = {
+ title: TITLE,
+ description: DESCRIPTION,
+ alternates: {
+ canonical: `${SEO.url}${PATH}`,
+ },
+ openGraph: {
+ title: TITLE,
+ description: DESCRIPTION,
+ url: `${SEO.url}${PATH}`,
+ siteName: SEO.name,
+ images: [
+ {
+ url: ogImageUrl(TITLE, OG_DESCRIPTION),
+ width: 1200,
+ height: 630,
+ alt: TITLE,
+ },
+ ],
+ locale: SEO.locale,
+ type: "website",
+ },
+ twitter: {
+ card: SEO.twitterCard,
+ title: TITLE,
+ description: DESCRIPTION,
+ images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
+ },
+ keywords: [
+ ...SEO.keywords,
+ "linear integration",
+ "issue to PR",
+ "AI in Linear",
+ "engineering workflow automation",
+ "Roo Code Cloud",
+ ],
+}
+
+// Invalidate cache when a request comes in, at most once every hour.
+export const revalidate = 3600
+
+type ValueProp = {
+ icon: LucideIcon
+ title: string
+ description: string
+}
+
+const VALUE_PROPS: ValueProp[] = [
+ {
+ icon: GitBranch,
+ title: "Work where you already work.",
+ description:
+ "Assign development work to @Roo Code directly from Linear. No new tools to learn, no context switching required.",
+ },
+ {
+ icon: Eye,
+ title: "Progress is visible.",
+ description:
+ "Watch progress unfold in real-time. Roo Code posts updates as comments, so your whole team stays in the loop.",
+ },
+ {
+ icon: MessageSquare,
+ title: "Mention for refinement.",
+ description:
+ 'Need changes? Just comment "@Roo Code also add dark mode support" and the agent picks up where it left off.',
+ },
+ {
+ icon: Link2,
+ title: "Full traceability.",
+ description:
+ "Every PR links back to the originating issue. Every issue shows its linked PR. Your audit trail stays clean.",
+ },
+ {
+ icon: Settings,
+ title: "Organization-level setup.",
+ description:
+ "Connect once, use everywhere. Your team members can assign issues to @Roo Code without individual configuration.",
+ },
+ {
+ icon: Shield,
+ title: "Safe by design.",
+ description:
+ "Agents never touch main/master directly. They produce branches and PRs. You review and approve before merge.",
+ },
+]
+
+// type WorkflowStep = {
+// step: number
+// title: string
+// description: string
+// }
+
+// const WORKFLOW_STEPS: WorkflowStep[] = [
+// {
+// step: 1,
+// title: "Create an issue",
+// description: "Write your issue with acceptance criteria. Be as detailed as you like.",
+// },
+// {
+// step: 2,
+// title: "Call @Roo Code",
+// description: "Mention @Roo Code in a comment to start. The agent begins working immediately.",
+// },
+// {
+// step: 3,
+// title: "Watch progress",
+// description: "Roo Code posts status updates as comments. Refine with @-mentions if needed.",
+// },
+// {
+// step: 4,
+// title: "Review the PR",
+// description: "When ready, the PR link appears in the issue. Review, iterate, and ship.",
+// },
+// ]
+
+type OnboardingStep = {
+ icon: LucideIcon
+ title: string
+ description: string
+ link?: {
+ href: string
+ text: string
+ }
+}
+
+const ONBOARDING_STEPS: OnboardingStep[] = [
+ {
+ icon: CreditCard,
+ title: "1. Team Plan",
+ description: "Linear integration requires a Team plan.",
+ link: {
+ href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
+ text: "Start a free trial",
+ },
+ },
+ {
+ icon: GitPullRequest,
+ title: "2. Connect GitHub",
+ description: "Link your repositories so Roo Code can open PRs on your behalf.",
+ },
+ {
+ icon: Settings,
+ title: "3. Connect Linear",
+ description: "Authorize via OAuth. No API keys to manage or rotate.",
+ },
+ {
+ icon: CheckCircle,
+ title: "4. Link & Start",
+ description: "Map your Linear project to a repo, then assign or mention @Roo Code.",
+ },
+]
+
+function LinearIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+export default function LinearPage(): JSX.Element {
+ return (
+ <>
+ {/* Hero Section */}
+
+
+
+
+
+
+
+ Powered by Roo Code Cloud
+
+
+ Turn Linear Issues into Pull Requests
+
+
+ Assign development work to @Roo Code directly from Linear. Get PRs back without
+ switching tools.
+
+
+
+
+
+
+
+
+
+
+
+ {/* Value Props Section */}
+
+
+
+
+
+ Why your team will love using Roo Code in Linear
+
+
+ AI agents that understand context, keep your team in the loop, and deliver PRs you can
+ review.
+
+
+
+ {VALUE_PROPS.map((prop, index) => {
+ const Icon = prop.icon
+ return (
+
+
+
+
+
{prop.title}
+
{prop.description}
+
+ )
+ })}
+
+
+
+
+ {/* Featured Workflow Section - temporarily commented out until video is ready
+
+
+
+
+
+
+
+ Featured Workflow
+
+
Issue to Shipped Feature
+
+ Stay in Linear from assignment to review. Roo Code keeps the issue updated and links the PR
+ when it's ready.
+
+
+
+
+
+ {/* YouTube Video Embed or Placeholder */}
+ {/*
+ {LINEAR_DEMO_YOUTUBE_ID ? (
+
+ ) : (
+
+
+
+ Demo Video Coming Soon
+
+
+ See the workflow in action: assign an issue to @Roo Code and watch as it
+ analyzes requirements, writes code, and opens a PR.
+
+
+ )}
+
+
+ {/* Workflow Steps */}
+ {/*
+ {WORKFLOW_STEPS.map((step) => (
+
+
+
+ {step.step}
+
+
+
+ {step.title}
+
+
+ {step.description}
+
+
+
+
+ ))}
+
+
+
+
+
+ */}
+
+ {/* Onboarding Section */}
+
+
+
+
Get started in minutes
+
+ Connect Linear and start assigning issues to AI.
+
+
+
+ {ONBOARDING_STEPS.map((step, index) => {
+ const Icon = step.icon
+ return (
+
+
+
+
+
{step.title}
+
+ {step.description}
+ {step.link && (
+ <>
+ {" "}
+
+ {step.link.text} →
+
+ >
+ )}
+
+
+ )
+ })}
+
+
+
+
+ {/* CTA Section */}
+
+
+
+
+ Start using Roo Code in Linear
+
+
+ Start a free 14 day Team trial.
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/web-roo-code/src/components/chromes/nav-bar.tsx b/apps/web-roo-code/src/components/chromes/nav-bar.tsx
index 023114c2d3f..fda49dfc871 100644
--- a/apps/web-roo-code/src/components/chromes/nav-bar.tsx
+++ b/apps/web-roo-code/src/components/chromes/nav-bar.tsx
@@ -15,6 +15,14 @@ import { ScrollButton } from "@/components/ui"
import ThemeToggle from "@/components/chromes/theme-toggle"
import { Brain, ChevronDown, Cloud, Puzzle, Slack, X } from "lucide-react"
+function LinearIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
interface NavBarProps {
stars: string | null
downloads: string | null
@@ -60,6 +68,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
Roo Code for Slack
+
+
+ Roo Code for Linear
+
@@ -202,6 +216,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
onClick={() => setIsMenuOpen(false)}>
Roo Code for Slack
+ setIsMenuOpen(false)}>
+ Roo Code for Linear
+
{
+ 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 (
+
+
+
+
+
+ )
+}
+
+function LinearIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+type ActivityRowProps = {
+ item: ActivityItem
+ isNew: boolean
+ reduceMotion: boolean
+}
+
+function ActivityRow({ item, isNew, reduceMotion }: ActivityRowProps): JSX.Element {
+ let animation = ""
+ if (!reduceMotion && isNew) {
+ animation = "animate-in fade-in slide-in-from-bottom-1 duration-300"
+ }
+
+ // Event items (status changes, etc.) - compact inline format
+ if (item.kind === "event") {
+ return (
+
+
+ {item.avatarText}
+
+
{item.author}
+
{item.body}
+
·
+
{item.timeLabel}
+
+ )
+ }
+
+ // PR link events
+ if (item.kind === "pr-link") {
+ return (
+
+
+ {item.body}
+ ·
+ {item.timeLabel}
+
+ )
+ }
+
+ // Comment items - more substantial with message body
+ return (
+
+
+ {item.avatarText}
+
+
+
+ {item.author}
+ ·
+ {item.timeLabel}
+
+
{item.body}
+
+
+ )
+}
+
+export type LinearIssueDemoProps = {
+ className?: string
+}
+
+export function LinearIssueDemo({ className }: LinearIssueDemoProps): JSX.Element {
+ const reduceMotion = usePrefersReducedMotion()
+ const [stepIndex, setStepIndex] = useState(0)
+ const scrollViewportRef = useRef(null)
+
+ const activityItems: ActivityItem[] = useMemo(
+ () => [
+ {
+ id: "a1",
+ kind: "comment",
+ author: "Jordan",
+ avatarText: "J",
+ avatarClassName: "bg-amber-600 text-white",
+ body: (
+
+ @Roo Code Can you implement this feature?
+
+ ),
+ timeLabel: "2m ago",
+ },
+ {
+ id: "a2",
+ kind: "comment",
+ author: "Roo Code",
+ avatarText: "R",
+ avatarClassName: "bg-indigo-600 text-white",
+ body: Analyzing issue requirements and codebase...,
+ timeLabel: "2m ago",
+ },
+ {
+ id: "a3",
+ kind: "event",
+ author: "Roo Code",
+ avatarText: "R",
+ avatarClassName: "bg-indigo-600 text-white",
+ body: moved to In Progress,
+ timeLabel: "2m ago",
+ },
+ {
+ id: "a4",
+ kind: "comment",
+ author: "Roo Code",
+ avatarText: "R",
+ avatarClassName: "bg-indigo-600 text-white",
+ body: Planning implementation: Settings component with light/dark toggle.,
+ timeLabel: "1m ago",
+ },
+ {
+ id: "a5",
+ kind: "comment",
+ author: "Jordan",
+ avatarText: "J",
+ avatarClassName: "bg-amber-600 text-white",
+ body: (
+
+ @Roo Code Please also add a "system" option
+ that follows OS preference.
+
+ ),
+ timeLabel: "1m ago",
+ },
+ {
+ id: "a6",
+ kind: "comment",
+ author: "Roo Code",
+ avatarText: "R",
+ avatarClassName: "bg-indigo-600 text-white",
+ body: (
+
+ Got it! Adding system preference detection using{" "}
+
+ prefers-color-scheme
+
+
+ ),
+ timeLabel: "30s ago",
+ },
+ {
+ id: "a7",
+ kind: "pr-link",
+ body: (
+
+ Roo Code linked{" "}
+ PR #847
+
+ ),
+ timeLabel: "just now",
+ },
+ {
+ id: "a8",
+ kind: "comment",
+ author: "Roo Code",
+ avatarText: "R",
+ avatarClassName: "bg-indigo-600 text-white",
+ body: (
+
+
+ PR ready for review:{" "}
+ #847
+
+
+
+
+ feat: add theme toggle with system preference
+
+
+142 -12 · 3 files changed
+
+
+ ),
+ timeLabel: "just now",
+ },
+ ],
+ [],
+ )
+
+ type DemoPhase =
+ | { kind: "issue" }
+ | { kind: "show"; activityIndex: number }
+ | { kind: "typing"; activityIndex: number }
+ | { kind: "reset" }
+
+ const phases: DemoPhase[] = useMemo(() => {
+ const next: DemoPhase[] = []
+
+ next.push({ kind: "issue" })
+
+ for (let activityIndex = 0; activityIndex < activityItems.length; activityIndex += 1) {
+ const item = activityItems[activityIndex]
+ if (item?.kind === "comment") {
+ next.push({ kind: "typing", activityIndex })
+ }
+ next.push({ kind: "show", activityIndex })
+ }
+ next.push({ kind: "reset" })
+ return next
+ }, [activityItems])
+
+ 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 = 2000
+ if (active?.kind === "reset") return 500
+ if (active?.kind === "issue") return 1500
+ if (active?.kind === "typing") return 800
+ return isLastMessageShow ? base * 2.5 : base
+ })()
+
+ const timer = window.setTimeout(() => {
+ const nextIndex = (stepIndex + 1) % phases.length
+ setStepIndex(nextIndex)
+ }, durationMs)
+
+ return () => window.clearTimeout(timer)
+ }, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
+
+ const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "issue" }
+
+ function getVisibleCount(phase: DemoPhase): number {
+ if (phase.kind === "reset" || phase.kind === "issue") return 0
+ if (phase.kind === "typing") return phase.activityIndex
+ return phase.activityIndex + 1
+ }
+
+ const visibleCount = getVisibleCount(activePhase)
+ const visibleActivities = activityItems.slice(0, visibleCount)
+ const typingTarget = activePhase.kind === "typing" ? activityItems[activePhase.activityIndex] : undefined
+
+ useEffect(() => {
+ const viewport = scrollViewportRef.current
+ if (!viewport) return
+
+ if (activePhase.kind === "reset" || activePhase.kind === "issue" || visibleCount <= 1) {
+ viewport.scrollTo({ top: 0, behavior: "auto" })
+ return
+ }
+
+ viewport.scrollTo({
+ top: viewport.scrollHeight,
+ behavior: reduceMotion ? "auto" : "smooth",
+ })
+ }, [activePhase.kind, reduceMotion, visibleCount])
+
+ const issueVisible = activePhase.kind !== "reset"
+
+ return (
+
+
+ {/* Linear-style Header with breadcrumb */}
+
+
+
Frontend
+
+
FE-312
+
+
+ Live demo
+
+
+
+ {/* Issue Content */}
+
+ {/* Issue Title */}
+
+
+ Add dark mode toggle to settings
+
+
+ Users should be able to switch between light and dark themes from the settings page. Persist
+ preference to localStorage and apply immediately.
+
+
+
+ {/* Activity Section */}
+
+
+ Activity
+ Unsubscribe
+
+
+
+ {visibleActivities.map((item) => (
+
+ ))}
+
+ {typingTarget && typingTarget.kind === "comment" && (
+
+
+ {typingTarget.avatarText}
+
+
+
+
+ {typingTarget.author}
+
+ typing
+
+
+
+
+ )}
+
+
+
+
+ {/* Comment Input */}
+
+
+
Leave a comment...
+
+
+
+
+
+
+ {/* Progress indicator */}
+
+
+ {activityItems.map((item, idx) => (
+
+ ))}
+
+
+
+
+ )
+}