diff --git a/apps/web-roo-code/public/illustrations/form-factor-cloud.png b/apps/web-roo-code/public/illustrations/form-factor-cloud.png new file mode 100644 index 0000000000..6746c96b01 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/form-factor-cloud.png differ diff --git a/apps/web-roo-code/public/illustrations/form-factor-extension.png b/apps/web-roo-code/public/illustrations/form-factor-extension.png new file mode 100644 index 0000000000..490728ec18 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/form-factor-extension.png differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/1.jpg b/apps/web-roo-code/public/illustrations/user-faces/1.jpg new file mode 100644 index 0000000000..71b358b156 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/1.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/10.jpg b/apps/web-roo-code/public/illustrations/user-faces/10.jpg new file mode 100644 index 0000000000..0a11fe5dc7 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/10.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/11.jpg b/apps/web-roo-code/public/illustrations/user-faces/11.jpg new file mode 100644 index 0000000000..1d6d3acde5 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/11.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/12.jpg b/apps/web-roo-code/public/illustrations/user-faces/12.jpg new file mode 100644 index 0000000000..5ffa4c1888 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/12.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/13.jpg b/apps/web-roo-code/public/illustrations/user-faces/13.jpg new file mode 100644 index 0000000000..24cd94ef30 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/13.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/14.jpg b/apps/web-roo-code/public/illustrations/user-faces/14.jpg new file mode 100644 index 0000000000..ab13a07b42 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/14.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/15.jpg b/apps/web-roo-code/public/illustrations/user-faces/15.jpg new file mode 100644 index 0000000000..b8dc44213b Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/15.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/16.jpg b/apps/web-roo-code/public/illustrations/user-faces/16.jpg new file mode 100644 index 0000000000..22652b2f40 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/16.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/17.jpg b/apps/web-roo-code/public/illustrations/user-faces/17.jpg new file mode 100644 index 0000000000..e6ac7ec84b Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/17.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/18.jpg b/apps/web-roo-code/public/illustrations/user-faces/18.jpg new file mode 100644 index 0000000000..21bfae28bf Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/18.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/19.jpg b/apps/web-roo-code/public/illustrations/user-faces/19.jpg new file mode 100644 index 0000000000..9f54700dbc Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/19.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/2.jpg b/apps/web-roo-code/public/illustrations/user-faces/2.jpg new file mode 100644 index 0000000000..9bf713c80d Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/2.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/20.jpg b/apps/web-roo-code/public/illustrations/user-faces/20.jpg new file mode 100644 index 0000000000..631f9c0730 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/20.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/21.jpg b/apps/web-roo-code/public/illustrations/user-faces/21.jpg new file mode 100644 index 0000000000..9f9a7aa335 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/21.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/22.jpg b/apps/web-roo-code/public/illustrations/user-faces/22.jpg new file mode 100644 index 0000000000..cef0eadcb1 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/22.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/23.jpg b/apps/web-roo-code/public/illustrations/user-faces/23.jpg new file mode 100644 index 0000000000..fafd86f6ea Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/23.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/24.jpg b/apps/web-roo-code/public/illustrations/user-faces/24.jpg new file mode 100644 index 0000000000..bd6813409f Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/24.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/3.jpg b/apps/web-roo-code/public/illustrations/user-faces/3.jpg new file mode 100644 index 0000000000..7ba55f1ed9 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/3.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/4.jpg b/apps/web-roo-code/public/illustrations/user-faces/4.jpg new file mode 100644 index 0000000000..98a3722df8 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/4.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/5.jpg b/apps/web-roo-code/public/illustrations/user-faces/5.jpg new file mode 100644 index 0000000000..0368881c09 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/5.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/6.jpg b/apps/web-roo-code/public/illustrations/user-faces/6.jpg new file mode 100644 index 0000000000..2e110c0b6f Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/6.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/7.jpg b/apps/web-roo-code/public/illustrations/user-faces/7.jpg new file mode 100644 index 0000000000..29a9134970 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/7.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/8.jpg b/apps/web-roo-code/public/illustrations/user-faces/8.jpg new file mode 100644 index 0000000000..a643823cd6 Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/8.jpg differ diff --git a/apps/web-roo-code/public/illustrations/user-faces/9.jpg b/apps/web-roo-code/public/illustrations/user-faces/9.jpg new file mode 100644 index 0000000000..4cfeaed27a Binary files /dev/null and b/apps/web-roo-code/public/illustrations/user-faces/9.jpg differ diff --git a/apps/web-roo-code/public/logos/bedrock.svg b/apps/web-roo-code/public/logos/bedrock.svg new file mode 100644 index 0000000000..a564939929 --- /dev/null +++ b/apps/web-roo-code/public/logos/bedrock.svg @@ -0,0 +1 @@ +Bedrock \ No newline at end of file diff --git a/apps/web-roo-code/public/logos/moonshot.svg b/apps/web-roo-code/public/logos/moonshot.svg new file mode 100644 index 0000000000..fb56ac106c --- /dev/null +++ b/apps/web-roo-code/public/logos/moonshot.svg @@ -0,0 +1 @@ +MoonshotAI \ No newline at end of file diff --git a/apps/web-roo-code/public/logos/openrouter.svg b/apps/web-roo-code/public/logos/openrouter.svg new file mode 100644 index 0000000000..e6cca2a869 --- /dev/null +++ b/apps/web-roo-code/public/logos/openrouter.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/apps/web-roo-code/src/app/layout.tsx b/apps/web-roo-code/src/app/layout.tsx index 5e510eb848..b96cef0eea 100644 --- a/apps/web-roo-code/src/app/layout.tsx +++ b/apps/web-roo-code/src/app/layout.tsx @@ -14,7 +14,7 @@ import "./globals.css" const inter = Inter({ subsets: ["latin"] }) const OG_TITLE = "Meet Roo Code" -const OG_DESCRIPTION = "The AI dev team that gets things done." +const OG_DESCRIPTION = "Your AI Software Engineering Team in the IDE and the Cloud." export const metadata: Metadata = { metadataBase: new URL(SEO.url), diff --git a/apps/web-roo-code/src/app/page.tsx b/apps/web-roo-code/src/app/page.tsx index b973e0cc70..f7ac47cf93 100644 --- a/apps/web-roo-code/src/app/page.tsx +++ b/apps/web-roo-code/src/app/page.tsx @@ -1,16 +1,12 @@ -/* eslint-disable react/jsx-no-target-blank */ - -import { getVSCodeDownloads } from "@/lib/stats" - import { Button } from "@/components/ui" import { - AnimatedBackground, - CodeExample, CompanyLogos, FAQSection, - Features, - InstallSection, Testimonials, + CTASection, + OptionOverviewSection, + PillarsSection, + UseExamplesSection, } from "@/components/homepage" import { EXTERNAL_LINKS } from "@/lib/constants" import { ArrowRight } from "lucide-react" @@ -20,70 +16,67 @@ import { StructuredData } from "@/components/structured-data" export const revalidate = 3600 export default async function Home() { - const downloads = await getVSCodeDownloads() - return ( <> -
- -
-
-
-
-

- The AI dev team that gets things done. -

-

- CoStrict's specialized modes stay on task and ship great code. Open source and - works with any model. -

-
- - +
+
+
+
+
+

+ Your AI Software Engineering Team is here. +
+ Interactive in the IDE, autonomous in the cloud. +

+
+

+ Use the Roo Code Extension on your computer for + full control, or delegate work to your{" "} + Roo Code Cloud Agents from the web, Slack, Github + or wherever your team is. +

+
+
+
+ + Free and Open Source
-
-
- -
+ +
+ + No credit card needed
+ +
+ +
-
- -
-
- -
-
- -
- + + + + + + + ) } 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 a01e1e8538..2442e3b759 100644 --- a/apps/web-roo-code/src/components/chromes/nav-bar.tsx +++ b/apps/web-roo-code/src/components/chromes/nav-bar.tsx @@ -13,7 +13,7 @@ import { EXTERNAL_LINKS } from "@/lib/constants" import { useLogoSrc } from "@/lib/hooks/use-logo-src" import { ScrollButton } from "@/components/ui" import ThemeToggle from "@/components/chromes/theme-toggle" -import { ChevronDown, Cloud, X } from "lucide-react" +import { ChevronDown, X } from "lucide-react" interface NavBarProps { stars: string | null @@ -104,6 +104,20 @@ export function NavBar({ stars, downloads }: NavBarProps) { {stars !== null && {stars}}
+ + Log in + + + Sign Up + {downloads !== null && {downloads}} - - - Log in -
{/* Mobile Menu Button */} @@ -226,15 +232,24 @@ export function NavBar({ stars, downloads }: NavBarProps) { {downloads !== null && {downloads}}
- setIsMenuOpen(false)}> - - Log in - +
+ setIsMenuOpen(false)}> + Sign up + + setIsMenuOpen(false)}> + Log in + +
diff --git a/apps/web-roo-code/src/components/homepage/cloud-section.tsx b/apps/web-roo-code/src/components/homepage/cloud-section.tsx new file mode 100644 index 0000000000..9b2539c4b0 --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/cloud-section.tsx @@ -0,0 +1,109 @@ +import { Bot, Settings2, ShieldCheck } from "lucide-react" + +export function CloudSection() { + return ( +
+
+
+

Asynchronous Engineering.

+

+ Stop watching the cursor. Deploy specialized agents to work while you sleep. +

+
+ + {/* Pipeline Diagram Visual */} +
+
Ticket
+
+
+ Planner Agent +
+
+
+ Coder Agent +
+
+
+ GitHub PR +
+
+ +
+
+
+
+ +
+

Purpose-Built Agents (Safety)

+
+

Zero Drift via Role Constraints

+

+ Fear of agents going haywire is solved by architecture, not prompt engineering. Cloud Agents + enforce the strict Modes you use locally. +

+
    +
  • + +
    + The Planner: + + Maps dependencies. Read-Only access. + +
    +
  • +
  • + +
    + The Builder: + + Writes code based on the plan. Scoped file access. + +
    +
  • +
  • + +
    + The Reviewer: + + Analyzes diffs. Cannot push to main. + +
    +
  • +
+
+ +
+
+
+ +
+

Orchestrated Configuration

+
+

Optimize Your AI Workforce

+

+ Just as you choose models locally, you configure them for the cloud to balance performance + vs. cost. +

+
+
Config Example:
+
+
+ Planner Agent + + o1-preview (Reasoning) + +
+
+ Unit Test Agent + + Haiku (Speed/Cost) + +
+
+
+
+
+
+
+ ) +} diff --git a/apps/web-roo-code/src/components/homepage/company-logos.tsx b/apps/web-roo-code/src/components/homepage/company-logos.tsx index a27e8bbc16..6aeb126ade 100644 --- a/apps/web-roo-code/src/components/homepage/company-logos.tsx +++ b/apps/web-roo-code/src/components/homepage/company-logos.tsx @@ -7,13 +7,13 @@ const logos = ["Apple", "Netflix", "Microsoft", "Amazon", "ByteDance", "Rakuten" export function CompanyLogos() { return ( -
+
- Making devs more productive at + className="text-xs text-muted-foreground text-center mb-2 "> + Helping teams ship more at
{logos.map((logo, index) => ( @@ -25,7 +25,7 @@ export function CompanyLogos() { {`${logo} diff --git a/apps/web-roo-code/src/components/homepage/cta-section.tsx b/apps/web-roo-code/src/components/homepage/cta-section.tsx new file mode 100644 index 0000000000..cd9a54487d --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/cta-section.tsx @@ -0,0 +1,37 @@ +import { Button } from "@/components/ui" +import { ArrowRight, Download } from "lucide-react" +import { EXTERNAL_LINKS } from "@/lib/constants" + +export function CTASection() { + return ( +
+
+

Build faster. Solo or Together.

+ + +
+
+ ) +} diff --git a/apps/web-roo-code/src/components/homepage/ecosystem-section.tsx b/apps/web-roo-code/src/components/homepage/ecosystem-section.tsx new file mode 100644 index 0000000000..8058512847 --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/ecosystem-section.tsx @@ -0,0 +1,82 @@ +import { GitMerge, Terminal, MessageSquare } from "lucide-react" + +export function EcosystemSection() { + return ( +
+
+

Integrated into your SDLC.

+ +
+ {/* Triangle Connection Lines - Absolute positioned */} +
+ + + + + +
+ +
+ {/* Step 1: Dispatch */} +
+
+ +
+
01. DISPATCH
+

Trigger Task

+

+ Trigger a task via @Roo in Slack or the VS Code terminal. +

+
+ + {/* Step 2: Execute */} +
+
+ +
+
02. EXECUTE
+

Run Agents

+

+ Agents run in isolated, ephemeral docker containers. +

+
+ + {/* Step 3: Merge */} +
+
+ +
+
03. MERGE
+

Review PR

+

+ The output is always a standard GitHub Pull Request. You review code, not chat logs. +

+
+
+
+
+
+ ) +} diff --git a/apps/web-roo-code/src/components/homepage/index.ts b/apps/web-roo-code/src/components/homepage/index.ts index 9d4427448e..faafc908cc 100644 --- a/apps/web-roo-code/src/components/homepage/index.ts +++ b/apps/web-roo-code/src/components/homepage/index.ts @@ -6,3 +6,9 @@ export * from "./features" export * from "./install-section" export * from "./testimonials" export * from "./whats-new-button" +export * from "./option-overview-section" +export * from "./pillars-section" +export * from "./cloud-section" +export * from "./ecosystem-section" +export * from "./cta-section" +export * from "./use-examples-section" diff --git a/apps/web-roo-code/src/components/homepage/option-overview-section.tsx b/apps/web-roo-code/src/components/homepage/option-overview-section.tsx new file mode 100644 index 0000000000..60e3f4bc76 --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/option-overview-section.tsx @@ -0,0 +1,99 @@ +import { Laptop, Cloud, ArrowRight } from "lucide-react" +import { Button } from "../ui" +import { EXTERNAL_LINKS } from "@/lib/constants" + +export function OptionOverviewSection() { + return ( +
+
+
+

+ Different form factors for different ways of working. +

+

+ Roo's always there to help you get stuff done. +

+
+
+
+
+
+ +
+
+ +
+

Roo Code VS Code Extension

+

For Individual Work

+ +
+

+ Run Roo directly in VS Code (or any fork – even Cursor!), stay close to the code and + control everything: +

+
    +
  • Approve every action (or set it to auto-approve)
  • +
  • Manage the context window
  • +
  • Configure every detail
  • +
  • Preview changes live
  • +
  • Stick to your customized editor
  • +
  • Write code by hand (gasp!)
  • +
+

+ Ideal for real-time debugging or quick iteration where you need full, immediate control. +

+
+ + +
+ +
+
+ +
+

Roo Code Cloud

+
For Team Work with Agents
+ +
+

+ Create your agent team in the Cloud, give them access to Github and start giving them + tasks: +

+
    +
  • + Use agents like the Planner, Coder, Explainer, Reviewer and Fixer +
  • +
  • Choose your provider and model
  • +
  • + Create tasks from the Web and Slack (more integrations soon) +
  • +
  • Get PR Reviews (and fixes) directly on Github
  • +
  • Collaborate with co-workers
  • +
+

+ Ideal for kicking projects off, parallelizing execution and looping in the rest of your + team. +

+
+ + +
+
+
+
+ ) +} diff --git a/apps/web-roo-code/src/components/homepage/pillars-section.tsx b/apps/web-roo-code/src/components/homepage/pillars-section.tsx new file mode 100644 index 0000000000..def772390f --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/pillars-section.tsx @@ -0,0 +1,202 @@ +import { Brain, Keyboard, Shield, Users2, Map, Code, MessageCircleQuestion, Bug, TestTube } from "lucide-react" +import Image from "next/image" +import { Link } from "../ui" + +const MODEL_LOGOS = [ + "OpenRouter", + "Anthropic", + "OpenAI", + "Gemini", + "Grok", + "Bedrock", + "Moonshot", + "Qwen", + "Kimi", + "Mistral", + "Ollama", +] +const MODE_EXAMPLES = [ + { + name: "Architect", + description: "Plans complex changes without making changes.", + icon: Map, + }, + { + name: "Code", + description: "Implements, refactors and optimizes code.", + icon: Code, + }, + { + name: "Ask", + description: "Explains functionality and program behavior.", + icon: MessageCircleQuestion, + }, + { + name: "Debug", + description: "Diagnoses issues, traces failures, and proposes targeted, reliable fixes.", + icon: Bug, + }, + { + name: "Test", + description: "Creates and improves performant tests without changing the actual functionality.", + icon: TestTube, + }, +] + +export function PillarsSection() { + return ( +
+
+
+
+
+
+

+ To trust an agent, you have to do it on your own terms. +

+

+ Roo is designed from the ground up to give you the confidence to do ever more with AI. +

+
+ +
+
+
+
+ +
+
+

Model-agnostic by design

+

Flexible and future-proof.

+
+

+ "The best model in the world" changes every other week. Providers + throttle models with no warning. 1st-party coding agents only work with their + own models. +

+

Roo doesn't care.

+

+ It works great with 10s of models, from frontier to open weight. Choose from{" "} + the curated selection we offer at-cost or + bring your own key. +

+
+
+ + Compatible with dozens of providers + +
+ {MODEL_LOGOS.map((logo, index) => ( + {`${logo} + ))} +
+
+
+
+
+ +
+
+
+ +
+
+

Role-specific Modes

+

On-task and under control.

+
+

+ As capable as they are, when let loose, LLMs hallucinate, cheat and can cause + real damage. +

+

+ Roo's Modes keep models focused on a given task and limit their access to + tools which are relevant to their role, keeping the context window clearer and + avoiding surprises. +

+

+ Modes are even smart enough to ask to switch to another when stepping outside + their responsibilities. +

+
+
+ Some examples +
    + {MODE_EXAMPLES.map((mode) => { + const Icon = mode.icon + return ( +
  • + +
    +

    {mode.name}

    +

    + {mode.description} +

    +
    +
  • + ) + })} +
+
+
+
+
+ +
+
+
+ +
+
+

Highly configurable

+

Make it fit your workflow.

+
+

+ Developer tools need to fit like gloves. Highly tweakable, + keyboard-shortcut-heavy gloves. +

+

We made Roo thoughtfully configurable to fit your workflow as best it can.

+
+
+
+
+ +
+
+
+ +
+
+

Secure and transparent

+

Open source from the get go.

+
+

+ The Roo Code Extension is{" "} + + open source + {" "} + so you can see for yourself exactly what it's doing and we don't use + your data for training. +

+

+ Plus we're fully SOC2 Type 2 compliant and follow industry-standard + security practices. +

+
+
+
+
+
+
+
+ ) +} diff --git a/apps/web-roo-code/src/components/homepage/testimonials.tsx b/apps/web-roo-code/src/components/homepage/testimonials.tsx index fe041324fc..9bec532c5c 100644 --- a/apps/web-roo-code/src/components/homepage/testimonials.tsx +++ b/apps/web-roo-code/src/components/homepage/testimonials.tsx @@ -193,11 +193,9 @@ export function Testimonials() {

- Developers really shipping with AI are using CoStrict + More than 1 million people are shipping with Roo.

-

- Join more than 1M people revolutionizing their workflow worldwide -

+

And they have some great things to say.

- {testimonial.role} at {testimonial.origin} + {testimonial.role !== "Reviewer" && ( + <> + {testimonial.role} at {testimonial.origin} + + )} {testimonial.stars && ( {" "} diff --git a/apps/web-roo-code/src/components/homepage/use-examples-section.tsx b/apps/web-roo-code/src/components/homepage/use-examples-section.tsx new file mode 100644 index 0000000000..9d22625413 --- /dev/null +++ b/apps/web-roo-code/src/components/homepage/use-examples-section.tsx @@ -0,0 +1,430 @@ +"use client" + +import { useMemo, useState } from "react" +import { motion, AnimatePresence } from "framer-motion" +import { + LucideIcon, + Pointer, + Slack, + Github, + Code, + GitPullRequest, + Wrench, + Map, + MessageCircleQuestionMark, + CornerDownRight, + ChevronDown, +} from "lucide-react" +import Image from "next/image" +import { Button } from "../ui" + +interface UseCase { + role: string + use: string + agent: UseCaseAgent + context: UseCaseSource +} + +interface UseCaseSource { + name: string + icon: LucideIcon +} + +interface UseCaseAgent { + name: string + icon: LucideIcon +} + +interface PositionedUseCase extends UseCase { + layer: 1 | 2 | 3 | 4 + position: { x: number; y: number } + scale: number + zIndex: number + avatar: string +} + +const SOURCES = { + slack: { + name: "Slack", + icon: Slack, + }, + web: { + name: "Web", + icon: Pointer, + }, + github: { + name: "Github", + icon: Github, + }, + extension: { + name: "Extension", + icon: Code, + }, +} + +const AGENTS = { + explainer: { + name: "Explainer", + icon: MessageCircleQuestionMark, + }, + planner: { + name: "Planner", + icon: Map, + }, + coder: { + name: "Coder", + icon: Code, + }, + reviewer: { + name: "Reviewer", + icon: GitPullRequest, + }, + fixer: { + name: "Fixer", + icon: Wrench, + }, +} + +const USE_CASES: UseCase[] = [ + { + role: "Frontend Developer", + use: "Take Lisa's feedback above and incorporate it into the landing page.", + agent: AGENTS.coder, + context: SOURCES.slack, + }, + { + role: "Customer Success", + use: "What could be causing this bug as described by the customer?", + agent: AGENTS.explainer, + context: SOURCES.web, + }, + { + role: "Backend Engineer", + use: "Create a migration denormalizing total_cost calculation and backfill the remainder.", + agent: AGENTS.coder, + context: SOURCES.extension, + }, + { + role: "Security Engineer", + use: "Do we use any of the libraries mentioned in the thread?", + agent: AGENTS.explainer, + context: SOURCES.slack, + }, + { + role: "Designer", + use: "Refactor the button component to use CSS variables", + agent: AGENTS.coder, + context: SOURCES.slack, + }, + { + role: "Product Manager", + use: "How big of a change would it be to turn this from a yes/no to have 4 options?", + agent: AGENTS.coder, + context: SOURCES.web, + }, + { + role: "QA Engineer", + use: "Write a Playwright test for the login flow failure case, extract existing mocks into shared.", + agent: AGENTS.coder, + context: SOURCES.github, + }, + { + role: "DevOps Engineer", + use: "Update the Dockerfile to use Node 20 Alpine.", + agent: AGENTS.fixer, + context: SOURCES.slack, + }, + { + role: "Mobile Developer", + use: "Copy what we did in PR #4253 and apply to this component.", + agent: AGENTS.coder, + context: SOURCES.slack, + }, + { + role: "Technical Writer", + use: "Generate JSDoc comments for the auth utility functions.", + agent: AGENTS.coder, + context: SOURCES.github, + }, + { + role: "Junior Developer", + use: "Review this pull request for potential performance improvements.", + agent: AGENTS.reviewer, + context: SOURCES.github, + }, + { + role: "Engineering Manager", + use: "Break down this user profile feature into technical tasks, grouped by skill.", + agent: AGENTS.planner, + context: SOURCES.web, + }, + { + role: "Support Engineer", + use: "What's causing this stack trace? The customer is on MacOS 26.1.", + agent: AGENTS.explainer, + context: SOURCES.web, + }, + { + role: "Frontend Developer", + use: "Make the navigation menu responsive on mobile devices.", + agent: AGENTS.coder, + context: SOURCES.web, + }, + { + role: "Backend Engineer", + use: "Give me two architecture options for the notification system in this PRD.", + agent: AGENTS.planner, + context: SOURCES.web, + }, + { + role: "Designer", + use: "Implement the loading spinner animation in CSS.", + agent: AGENTS.coder, + context: SOURCES.web, + }, + { + role: "Customer Success", + use: "Write a script to find patterns in these CPU load logs.", + agent: AGENTS.coder, + context: SOURCES.slack, + }, + { + role: "Full Stack Dev", + use: "Refactor user_preferences to use named columns instead of a single JSON blob", + agent: AGENTS.coder, + context: SOURCES.extension, + }, + { + role: "QA Engineer", + use: "Automate the regression suite for the checkout process.", + agent: AGENTS.coder, + context: SOURCES.extension, + }, + { + role: "DevOps Engineer", + use: "Understand why this build error only happens in prod and fix it.", + agent: AGENTS.coder, + context: SOURCES.extension, + }, + { + role: "Product Marketer", + use: "What were the 5 most significant PRs merged in the past week?", + agent: AGENTS.explainer, + context: SOURCES.slack, + }, + { + role: "Junior Developer", + use: "Explain how useEffect dependency arrays work here.", + agent: AGENTS.explainer, + context: SOURCES.extension, + }, + { + role: "Senior Engineer", + use: "Check if this implementation follows the Single Responsibility Principle.", + agent: AGENTS.reviewer, + context: SOURCES.github, + }, +] + +// Seeded random number generator for consistent layout +function seededRandom(seed: number) { + let value = seed + return () => { + value = (value * 9301 + 49297) % 233280 + return value / 233280 + } +} + +const LAYER_SCALES = { + 1: 0.7, + 2: 0.85, + 3: 1.0, + 4: 1.15, +} + +function distributeItems(items: UseCase[]): PositionedUseCase[] { + const rng = seededRandom(Math.random() * 12345) + const zones = { rows: 7, cols: 4 } + const zoneWidth = 100 / zones.cols + const zoneHeight = 100 / zones.rows + + // Create array of zone indices [0...19] and shuffle them + const zoneIndices = Array.from({ length: items.length }, (_, i) => i) + for (let i = zoneIndices.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)) + const temp = zoneIndices[i]! + zoneIndices[i] = zoneIndices[j]! + zoneIndices[j] = temp + } + + return items.map((item, index) => { + // Assign to a random unique zone + const zoneIndex = zoneIndices[index]! + const row = Math.floor(zoneIndex / zones.cols) + const col = zoneIndex % zones.cols + + // Distribute layers evenly + const layer = ((index % 4) + 1) as 1 | 2 | 3 | 4 + + // Calculate base position (center of zone) + const baseX = col * zoneWidth + zoneWidth / 2 + const baseY = row * zoneHeight + zoneHeight / 2 + + // Add jitter (±35% of zone size to keep somewhat contained but messy) + const jitterX = (rng() - 0.5) * zoneWidth * 0.7 + const jitterY = (rng() - 0.5) * zoneHeight * 0.7 + + return { + ...item, + avatar: `/illustrations/user-faces/${index + 1}.jpg`, + layer, + position: { + x: baseX + jitterX, + y: baseY + jitterY, + }, + scale: LAYER_SCALES[layer], + zIndex: layer, + } + }) +} + +function UseCaseCardContent({ + item, + opacity = 1, + className = "", +}: { + item: UseCase & { avatar: string } + opacity?: number + className?: string +}) { + const ContextIcon: LucideIcon = item.context.icon + return ( +

+
+ + {item.role} +
+ +
+ + To {item.agent.name} Agent +
+ +
+ {item.use} +
+ +
+ via {item.context.name} +
+
+ ) +} + +function DesktopUseCaseCard({ item }: { item: PositionedUseCase }) { + const opacity = Math.min(1, 0.5 + item.layer / 3) + + return ( + `translate(-50%, -50%) scale(${scale})`}> + + + ) +} + +export function UseExamplesSection() { + const positionedItems = useMemo(() => distributeItems(USE_CASES), []) + const [showAllMobile, setShowAllMobile] = useState(false) + + return ( +
+
+
+
+
+
+

+ The AI team to help your entire human team +

+

+ Developers, PMs, Designers, Customer Success: everyone moves faster and more independently with + Roo. +

+
+ + {/* Mobile: Vertical Staggered List */} +
+ + {positionedItems.slice(0, showAllMobile ? undefined : 8).map((item, index) => ( + + + + ))} + + + {!showAllMobile && ( +
+ +
+ )} +
+ + {/* Desktop: Positioned Items Container */} +
+ {positionedItems.map((item, index) => ( + + ))} +
+
+
+ ) +} diff --git a/apps/web-roo-code/src/components/ui/button.tsx b/apps/web-roo-code/src/components/ui/button.tsx index 18324ad791..2884f45c81 100644 --- a/apps/web-roo-code/src/components/ui/button.tsx +++ b/apps/web-roo-code/src/components/ui/button.tsx @@ -5,12 +5,12 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + default: "bg-primary text-primary-foreground shadow hover:bg-primary/80", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", @@ -18,8 +18,9 @@ const buttonVariants = cva( }, size: { default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", + sm: "h-8 px-3 text-xs", + lg: "h-10 px-8", + xl: "h-14 px-8 text-lg", icon: "h-9 w-9", }, }, diff --git a/apps/web-roo-code/src/components/ui/index.ts b/apps/web-roo-code/src/components/ui/index.ts index 737046e479..18420eee0c 100644 --- a/apps/web-roo-code/src/components/ui/index.ts +++ b/apps/web-roo-code/src/components/ui/index.ts @@ -3,3 +3,4 @@ export * from "./chart" export * from "./modal" export * from "./scroll-button" export * from "./table" +export * from "./link" diff --git a/apps/web-roo-code/src/components/ui/link.tsx b/apps/web-roo-code/src/components/ui/link.tsx new file mode 100644 index 0000000000..2a2cbf4375 --- /dev/null +++ b/apps/web-roo-code/src/components/ui/link.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import NextLink from "next/link" + +import { cn } from "@/lib/utils" + +type BaseLinkProps = React.ComponentPropsWithoutRef + +type LinkProps = BaseLinkProps & { + newWindow?: boolean +} + +const Link = React.forwardRef, LinkProps>( + ({ className, newWindow = false, target, rel, ...props }, ref) => { + const computedTarget = newWindow ? "_blank" : target + const computedRel = newWindow + ? rel + ? rel.includes("noreferrer") + ? rel + : `${rel} noreferrer` + : "noreferrer" + : rel + + return ( + + ) + }, +) + +Link.displayName = "Link" + +export { Link } +export type { LinkProps } diff --git a/apps/web-roo-code/src/lib/constants.ts b/apps/web-roo-code/src/lib/constants.ts index 084c214634..18d605107b 100644 --- a/apps/web-roo-code/src/lib/constants.ts +++ b/apps/web-roo-code/src/lib/constants.ts @@ -26,6 +26,7 @@ export const EXTERNAL_LINKS = { TESTIMONIALS: "https://roocode.com/#testimonials", CLOUD_APP_LOGIN: "https://app.roocode.com/sign-in", CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up", + CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup", CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup", } diff --git a/apps/web-roo-code/src/lib/stats.ts b/apps/web-roo-code/src/lib/stats.ts index e479ce5010..9ce2714109 100644 --- a/apps/web-roo-code/src/lib/stats.ts +++ b/apps/web-roo-code/src/lib/stats.ts @@ -107,7 +107,7 @@ function formatNumber(num: number): string { // if number is 1 million or more, format as millions if (num >= 1000000) { const truncated = Math.floor((num / 1000000) * 10) / 10 - return truncated.toFixed(1) + "M" + return truncated.toFixed(2) + "M" } // otherwise, format as thousands diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index e908886515..8aaa489be4 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -112,6 +112,8 @@ export const modelInfoSchema = z.object({ cachableFields: z.array(z.string()).optional(), // Flag to indicate if the model is deprecated and should not be used deprecated: z.boolean().optional(), + // Flag to indicate if the model should hide vendor/company identity in responses + isStealthModel: z.boolean().optional(), // Flag to indicate if the model is free (no cost) isFree: z.boolean().optional(), // Flag to indicate if the model supports native tool calling (OpenAI-style function calling) diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index 3dcc352b1d..e21fcc698b 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -17,7 +17,6 @@ export const internationalZAiModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, - supportsReasoningBinary: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -94,7 +93,6 @@ export const internationalZAiModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, - supportsReasoningBinary: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -125,7 +123,6 @@ export const mainlandZAiModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, - supportsReasoningBinary: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, @@ -202,7 +199,6 @@ export const mainlandZAiModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, - supportsReasoningBinary: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index 15f3fef911..083fdc13ef 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -295,145 +295,5 @@ describe("ZAiHandler", () => { undefined, ) }) - - describe("Reasoning functionality", () => { - it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in createMessage", async () => { - const handlerWithReasoning = new ZAiHandler({ - apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true - zaiApiKey: "test-zai-api-key", - zaiApiLine: "international_coding", - enableReasoningEffort: true, - }) - - mockCreate.mockImplementationOnce(() => { - return { - [Symbol.asyncIterator]: () => ({ - async next() { - return { done: true } - }, - }), - } - }) - - const systemPrompt = "Test system prompt" - const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] - - const messageGenerator = handlerWithReasoning.createMessage(systemPrompt, messages) - await messageGenerator.next() - - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - thinking: { type: "enabled" }, - }), - undefined, - ) - }) - - it("should not include thinking parameter when enableReasoningEffort is false in createMessage", async () => { - const handlerWithoutReasoning = new ZAiHandler({ - apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true - zaiApiKey: "test-zai-api-key", - zaiApiLine: "international_coding", - enableReasoningEffort: false, - }) - - mockCreate.mockImplementationOnce(() => { - return { - [Symbol.asyncIterator]: () => ({ - async next() { - return { done: true } - }, - }), - } - }) - - const systemPrompt = "Test system prompt" - const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] - - const messageGenerator = handlerWithoutReasoning.createMessage(systemPrompt, messages) - await messageGenerator.next() - - expect(mockCreate).toHaveBeenCalledWith( - expect.not.objectContaining({ - thinking: expect.anything(), - }), - undefined, - ) - }) - - it("should not include thinking parameter when model does not support reasoning in createMessage", async () => { - const handlerWithNonReasoningModel = new ZAiHandler({ - apiModelId: "glm-4-32b-0414-128k", // This model doesn't have supportsReasoningBinary: true - zaiApiKey: "test-zai-api-key", - zaiApiLine: "international_coding", - enableReasoningEffort: true, - }) - - mockCreate.mockImplementationOnce(() => { - return { - [Symbol.asyncIterator]: () => ({ - async next() { - return { done: true } - }, - }), - } - }) - - const systemPrompt = "Test system prompt" - const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] - - const messageGenerator = handlerWithNonReasoningModel.createMessage(systemPrompt, messages) - await messageGenerator.next() - - expect(mockCreate).toHaveBeenCalledWith( - expect.not.objectContaining({ - thinking: expect.anything(), - }), - undefined, - ) - }) - - it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in completePrompt", async () => { - const handlerWithReasoning = new ZAiHandler({ - apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true - zaiApiKey: "test-zai-api-key", - zaiApiLine: "international_coding", - enableReasoningEffort: true, - }) - - const expectedResponse = "This is a test response" - mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) - - await handlerWithReasoning.completePrompt("test prompt") - - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - thinking: { type: "enabled" }, - }), - { signal: undefined }, - ) - }) - - it("should not include thinking parameter when enableReasoningEffort is false in completePrompt", async () => { - const handlerWithoutReasoning = new ZAiHandler({ - apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true - zaiApiKey: "test-zai-api-key", - zaiApiLine: "international_coding", - enableReasoningEffort: false, - }) - - const expectedResponse = "This is a test response" - mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) - - await handlerWithoutReasoning.completePrompt("test prompt") - - expect(mockCreate).toHaveBeenCalledWith( - expect.not.objectContaining({ - thinking: expect.anything(), - }), - { signal: undefined }, - ) - }) - }) }) }) diff --git a/src/api/providers/fetchers/__tests__/roo.spec.ts b/src/api/providers/fetchers/__tests__/roo.spec.ts index c557dedd48..84d7284cfa 100644 --- a/src/api/providers/fetchers/__tests__/roo.spec.ts +++ b/src/api/providers/fetchers/__tests__/roo.spec.ts @@ -645,4 +645,70 @@ describe("getRooModels", () => { expect(models["test/non-native-model"].supportsNativeTools).toBe(true) expect(models["test/non-native-model"].defaultToolProtocol).toBeUndefined() }) + + it("should detect stealth mode from tags", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/stealth-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Stealth Model", + description: "Model with stealth mode", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: ["stealth"], + pricing: { + input: "0.0001", + output: "0.0002", + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + + expect(models["test/stealth-model"].isStealthModel).toBe(true) + }) + + it("should not set isStealthModel when stealth tag is absent", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/non-stealth-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Non-Stealth Model", + description: "Model without stealth mode", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: [], + pricing: { + input: "0.0001", + output: "0.0002", + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + + expect(models["test/non-stealth-model"].isStealthModel).toBeUndefined() + }) }) diff --git a/src/api/providers/fetchers/roo.ts b/src/api/providers/fetchers/roo.ts index 42b30139f6..004ff84cc0 100644 --- a/src/api/providers/fetchers/roo.ts +++ b/src/api/providers/fetchers/roo.ts @@ -115,6 +115,9 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise } break diff --git a/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts b/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts index d1347b3794..bea4eeed91 100644 --- a/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts +++ b/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts @@ -179,7 +179,7 @@ describe("AssistantMessageParser (streaming)", () => { // This has XML-like content: return true; } - 5` + ` const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) @@ -188,7 +188,6 @@ describe("AssistantMessageParser (streaming)", () => { expect(toolUse.type).toBe("tool_use") expect(toolUse.name).toBe("write_to_file") expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.params.line_count).toBe("5") expect(toolUse.params.content).toContain("function example()") expect(toolUse.params.content).toContain("// This has XML-like content: ") expect(toolUse.params.content).toContain("return true;") @@ -263,7 +262,7 @@ describe("AssistantMessageParser (streaming)", () => { line 1 line 2 line 3 - 3` + ` const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) expect(result).toHaveLength(1) @@ -274,7 +273,6 @@ describe("AssistantMessageParser (streaming)", () => { expect(toolUse.params.content).toContain("line 1") expect(toolUse.params.content).toContain("line 2") expect(toolUse.params.content).toContain("line 3") - expect(toolUse.params.line_count).toBe("3") expect(toolUse.partial).toBe(false) }) it("should handle a complex message with multiple content types", () => { @@ -287,7 +285,7 @@ describe("AssistantMessageParser (streaming)", () => { src/index.ts // Updated content console.log("Hello world"); - 2 + Let's run the code: diff --git a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts b/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts index f5ae600bee..80d2502626 100644 --- a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts +++ b/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts @@ -168,7 +168,7 @@ const isEmptyTextContent = (block: AssistantMessageContent) => // This has XML-like content: return true; } - 5` + ` const result = parser(message).filter((block) => !isEmptyTextContent(block)) @@ -177,7 +177,6 @@ const isEmptyTextContent = (block: AssistantMessageContent) => expect(toolUse.type).toBe("tool_use") expect(toolUse.name).toBe("write_to_file") expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.params.line_count).toBe("5") expect(toolUse.params.content).toContain("function example()") expect(toolUse.params.content).toContain("// This has XML-like content: ") expect(toolUse.params.content).toContain("return true;") @@ -276,7 +275,7 @@ const isEmptyTextContent = (block: AssistantMessageContent) => line 1 line 2 line 3 - 3` + ` const result = parser(message).filter((block) => !isEmptyTextContent(block)) expect(result).toHaveLength(1) @@ -287,7 +286,6 @@ const isEmptyTextContent = (block: AssistantMessageContent) => expect(toolUse.params.content).toContain("line 1") expect(toolUse.params.content).toContain("line 2") expect(toolUse.params.content).toContain("line 3") - expect(toolUse.params.line_count).toBe("3") expect(toolUse.partial).toBe(false) }) @@ -301,7 +299,7 @@ const isEmptyTextContent = (block: AssistantMessageContent) => src/index.ts // Updated content console.log("Hello world"); - 2 + Let's run the code: diff --git a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts b/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts index d5450988c9..a32b1173ce 100644 --- a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts +++ b/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts @@ -62,17 +62,17 @@ const testCases = [ }, { name: "Message with a complex tool use (write_to_file)", - input: "src/file.ts\nfunction example() {\n // This has XML-like content: \n return true;\n}\n5", + input: "src/file.ts\nfunction example() {\n // This has XML-like content: \n return true;\n}\n", }, { name: "Message with multiple tool uses", - input: "First file: src/file1.ts\nSecond file: src/file2.ts\nLet's write a new file: src/file3.ts\nexport function newFunction() {\n return 'Hello world';\n}\n3", + input: "First file: src/file1.ts\nSecond file: src/file2.ts\nLet's write a new file: src/file3.ts\nexport function newFunction() {\n return 'Hello world';\n}\n", }, { name: "Large message with repeated tool uses", input: Array(50) .fill( - 'src/file.ts\noutput.tsconsole.log("hello");1', + 'src/file.ts\noutput.tsconsole.log("hello");', ) .join("\n"), }, diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 8a3eb51d8f..fe3aa8ce1e 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -492,6 +492,7 @@ export async function presentAssistantMessage(cline: Task) { ) const pushToolResult = (content: ToolResponse) => { + const editTools = ["insert_content", "apply_diff", "search_and_replace", "apply_patch", "write_to_file"] if (toolProtocol === TOOL_PROTOCOL.NATIVE) { // For native protocol, only allow ONE tool_result per tool call if (hasToolResult) { @@ -532,10 +533,7 @@ export async function presentAssistantMessage(cline: Task) { } hasToolResult = true - if ( - ["write_to_file", "apply_diff", "insert_content"].includes(block.name) && - block.partial === false - ) { + if (editTools.includes(block.name) && block.partial === false) { updateCospecMetadata(cline, block?.params?.path) } } else { @@ -550,10 +548,7 @@ export async function presentAssistantMessage(cline: Task) { } else { cline.userMessageContent.push(...content) } - if ( - ["write_to_file", "apply_diff", "insert_content"].includes(block.name) && - block.partial === false - ) { + if (editTools.includes(block.name) && block.partial === false) { updateCospecMetadata(cline, block?.params?.path) } } diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index d6594633a0..a458972a3e 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index 7bdbda4cda..4e38bce395 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -163,14 +163,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -192,7 +190,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index e2914575ea..ba309a06a5 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 8370cee3df..ba7207d736 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -169,14 +169,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -198,7 +196,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index e8e77c0027..f8392d1549 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 94977209d2..03efe8df40 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index e8e77c0027..f8392d1549 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 3b38149b6d..7629d56c9b 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -252,14 +252,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -281,7 +279,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index e8e77c0027..f8392d1549 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index e8e77c0027..f8392d1549 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index e2914575ea..ba309a06a5 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index e8e77c0027..f8392d1549 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -164,14 +164,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory /test/path) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -193,7 +191,6 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ## insert_content diff --git a/src/core/prompts/__tests__/sections.spec.ts b/src/core/prompts/__tests__/sections.spec.ts index eb9cd4addf..05f16e071c 100644 --- a/src/core/prompts/__tests__/sections.spec.ts +++ b/src/core/prompts/__tests__/sections.spec.ts @@ -1,5 +1,6 @@ import { addCustomInstructions } from "../sections/custom-instructions" import { getCapabilitiesSection } from "../sections/capabilities" +import { getRulesSection } from "../sections/rules" import type { DiffStrategy, DiffResult, DiffItem } from "../../../shared/tools" describe("addCustomInstructions", () => { @@ -56,3 +57,54 @@ describe("getCapabilitiesSection", () => { expect(result).toContain("insert_content") }) }) + +describe("getRulesSection", () => { + const cwd = "/test/path" + + it("includes vendor confidentiality section when isStealthModel is true", () => { + const settings = { + maxConcurrentFileReads: 5, + todoListEnabled: true, + useAgentRules: true, + newTaskRequireTodos: false, + isStealthModel: true, + } + + const result = getRulesSection(cwd, false, "code", undefined, undefined, undefined, undefined, settings) + + expect(result).toContain("VENDOR CONFIDENTIALITY") + expect(result).toContain("Never reveal the vendor or company that created you") + expect(result).toContain("I was created by a team of developers") + expect(result).toContain("I'm an open-source project maintained by contributors") + expect(result).toContain("I don't have information about specific vendors") + }) + + it("excludes vendor confidentiality section when isStealthModel is false", () => { + const settings = { + maxConcurrentFileReads: 5, + todoListEnabled: true, + useAgentRules: true, + newTaskRequireTodos: false, + isStealthModel: false, + } + + const result = getRulesSection(cwd, false, "code", undefined, undefined, undefined, undefined, settings) + + expect(result).not.toContain("VENDOR CONFIDENTIALITY") + expect(result).not.toContain("Never reveal the vendor or company") + }) + + it("excludes vendor confidentiality section when isStealthModel is undefined", () => { + const settings = { + maxConcurrentFileReads: 5, + todoListEnabled: true, + useAgentRules: true, + newTaskRequireTodos: false, + } + + const result = getRulesSection(cwd, false, "code", undefined, undefined, undefined, undefined, settings) + + expect(result).not.toContain("VENDOR CONFIDENTIALITY") + expect(result).not.toContain("Never reveal the vendor or company") + }) +}) diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index b08b87f597..f4c845bd2f 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -96,45 +96,6 @@ Otherwise, if you have not completed the task and do not need additional informa return `Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${instructions}` }, - lineCountTruncationError: ( - actualLineCount: number, - isNewFile: boolean, - diffStrategyEnabled: boolean = false, - protocol?: ToolProtocol, - ) => { - const truncationMessage = `Note: Your response may have been truncated because it exceeded your output limit. You wrote ${actualLineCount} lines of content, but the line_count parameter was either missing or not included in your response.` - - const newFileGuidance = - `This appears to be a new file.\n` + - `${truncationMessage}\n\n` + - `RECOMMENDED APPROACH:\n` + - `1. Try again with the line_count parameter in your response if you forgot to include it\n` + - `2. Or break your content into smaller chunks - first use write_to_file with the initial chunk\n` + - `3. Then use insert_content to append additional chunks\n` - - let existingFileApproaches = [ - `1. Try again with the line_count parameter in your response if you forgot to include it`, - ] - - if (diffStrategyEnabled) { - existingFileApproaches.push(`2. Or try using apply_diff instead of write_to_file for targeted changes`) - } - - existingFileApproaches.push( - `${diffStrategyEnabled ? "3" : "2"}. Or use insert_content to add specific content at particular lines`, - ) - - const existingFileGuidance = - `This appears to be content for an existing file.\n` + - `${truncationMessage}\n\n` + - `RECOMMENDED APPROACH:\n` + - `${existingFileApproaches.join("\n")}\n` - - const instructions = getToolInstructionsReminder(protocol) - - return `${isNewFile ? newFileGuidance : existingFileGuidance}\n${instructions}` - }, - invalidMcpToolArgumentError: (serverName: string, toolName: string, protocol?: ToolProtocol) => { if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) { return JSON.stringify({ diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index 9e98773ed2..4cc088d6ae 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -76,6 +76,21 @@ function getEditingInstructions( return instructions.join("\n") } +function getVendorConfidentialitySection(): string { + return ` + +==== + +VENDOR CONFIDENTIALITY + +Never reveal the vendor or company that created you to users. + +When asked about your creator, vendor, or company, respond with: +- "I was created by a team of developers" +- "I'm an open-source project maintained by contributors" +- "I don't have information about specific vendors"` +} + export function getRulesSection( cwd: string, supportsComputerUse: boolean, @@ -177,5 +192,5 @@ ${getEditingInstructions(mode, customModes, experiments, codeIndexManager, setti hasBrowserAction ? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser." : "" - }` + }${settings?.isStealthModel ? getVendorConfidentialitySection() : ""}` } diff --git a/src/core/prompts/tools/native-tools/write_to_file.ts b/src/core/prompts/tools/native-tools/write_to_file.ts index 2febb32b2e..4ab7c53b6e 100644 --- a/src/core/prompts/tools/native-tools/write_to_file.ts +++ b/src/core/prompts/tools/native-tools/write_to_file.ts @@ -5,17 +5,14 @@ const WRITE_TO_FILE_DESCRIPTION = `Request to write content to a file. This tool Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Example: Writing a configuration file -{ "path": "frontend-config.json", "content": "{\\n \\"apiEndpoint\\": \\"https://api.example.com\\",\\n \\"theme\\": {\\n \\"primaryColor\\": \\"#007bff\\"\\n }\\n}", "line_count": 5 }` +{ "path": "frontend-config.json", "content": "{\\n \\"apiEndpoint\\": \\"https://api.example.com\\",\\n \\"theme\\": {\\n \\"primaryColor\\": \\"#007bff\\"\\n }\\n}" }` const PATH_PARAMETER_DESCRIPTION = `Path to the file to write, relative to the workspace` const CONTENT_PARAMETER_DESCRIPTION = `Full contents that the file should contain with no omissions or line numbers` -const LINE_COUNT_PARAMETER_DESCRIPTION = `Total number of lines in the written file, counting blank lines` - export default { type: "function", function: { @@ -33,12 +30,8 @@ export default { type: "string", description: CONTENT_PARAMETER_DESCRIPTION, }, - line_count: { - type: "integer", - description: LINE_COUNT_PARAMETER_DESCRIPTION, - }, }, - required: ["path", "content", "line_count"], + required: ["path", "content"], additionalProperties: false, }, }, diff --git a/src/core/prompts/tools/write-to-file.ts b/src/core/prompts/tools/write-to-file.ts index 221103b04f..c957f0891e 100644 --- a/src/core/prompts/tools/write-to-file.ts +++ b/src/core/prompts/tools/write-to-file.ts @@ -6,14 +6,12 @@ Description: Request to write content to a file. This tool is primarily used for Parameters: - path: (required) The path of the file to write to (relative to the current workspace directory ${args.cwd}) - content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. Usage: File path here Your file content here -total number of lines in the file, including empty lines Example: Requesting to write to frontend-config.json @@ -35,6 +33,5 @@ Example: Requesting to write to frontend-config.json "version": "1.0.0" } -14 ` } diff --git a/src/core/prompts/types.ts b/src/core/prompts/types.ts index cab5228f08..30cc4e4700 100644 --- a/src/core/prompts/types.ts +++ b/src/core/prompts/types.ts @@ -11,4 +11,6 @@ export interface SystemPromptSettings { newTaskRequireTodos: boolean terminalShellIntegrationDisabled?: boolean toolProtocol?: ToolProtocol + /** When true, model should hide vendor/company identity in responses */ + isStealthModel?: boolean } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index a3a399ff99..13580c32a9 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2277,8 +2277,7 @@ export class Task extends EventEmitter implements TaskLike { // Add environment details as its own text block, separate from tool // results. - const finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }] - + let finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }] // Only add user message to conversation history if: // 1. This is the first attempt (retryAttempt === 0), AND // 2. The original userContent was not empty (empty signals delegation resume where @@ -3383,6 +3382,7 @@ export class Task extends EventEmitter implements TaskLike { .getConfiguration(Package.name) .get("newTaskRequireTodos", false), toolProtocol, + isStealthModel: modelInfo?.isStealthModel, }, undefined, // todoList this.api.getModel().id, diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index f7d7d2c6a6..f1bae7ff2f 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -18,12 +18,10 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" -import { resolveToolProtocol } from "../../utils/resolveToolProtocol" interface WriteToFileParams { path: string content: string - line_count: number } export class WriteToFileTool extends BaseTool<"write_to_file"> { @@ -33,15 +31,13 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { return { path: params.path || "", content: params.content || "", - line_count: parseInt(params.line_count ?? "0", 10), } } async execute(params: WriteToFileParams, task: Task, callbacks: ToolCallbacks): Promise { - const { pushToolResult, handleError, askApproval, removeClosingTag, toolProtocol } = callbacks + const { pushToolResult, handleError, askApproval, removeClosingTag } = callbacks const relPath = params.path let newContent = params.content - const predictedLineCount = params.line_count if (!relPath) { task.consecutiveMistakeCount++ @@ -63,7 +59,7 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { if (!accessAllowed) { await task.say("rooignore_error", relPath) - pushToolResult(formatResponse.rooIgnoreError(relPath, toolProtocol)) + pushToolResult(formatResponse.rooIgnoreError(relPath)) return } @@ -109,38 +105,6 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { } try { - if (predictedLineCount === undefined || predictedLineCount === 0) { - task.consecutiveMistakeCount++ - task.recordToolError("write_to_file") - task.didToolFailInCurrentTurn = true - - const actualLineCount = newContent.split("\n").length - const isNewFile = !fileExists - const diffStrategyEnabled = !!task.diffStrategy - const modelInfo = task.api.getModel().info - const toolProtocol = resolveToolProtocol(task.apiConfiguration, modelInfo) - - await task.say( - "error", - `Roo tried to use write_to_file${ - relPath ? ` for '${relPath.toPosix()}'` : "" - } but the required parameter 'line_count' was missing or truncated after ${actualLineCount} lines of content were written. Retrying...`, - ) - - pushToolResult( - formatResponse.toolError( - formatResponse.lineCountTruncationError( - actualLineCount, - isNewFile, - diffStrategyEnabled, - toolProtocol, - ), - ), - ) - await task.diffViewProvider.revertChanges() - return - } - task.consecutiveMistakeCount = 0 const provider = task.providerRef.deref() @@ -161,24 +125,22 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { task.diffViewProvider.originalContent = "" } - if (detectCodeOmission(task.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (detectCodeOmission(task.diffViewProvider.originalContent || "", newContent)) { if (task.diffStrategy) { pushToolResult( formatResponse.toolError( - `Content appears to be truncated (file has ${ - newContent.split("\n").length - } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, + `Content appears to contain comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, ), ) return } else { vscode.window .showWarningMessage( - "Potential code truncation detected. cline happens when the AI reaches its max output limit.", - "Follow cline guide to fix the issue", + "Potential code truncation detected. This happens when the AI reaches its max output limit.", + "Follow guide to fix the issue", ) .then((selection) => { - if (selection === "Follow cline guide to fix the issue") { + if (selection === "Follow guide to fix the issue") { vscode.env.openExternal( vscode.Uri.parse( "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", @@ -221,26 +183,24 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { await delay(300) task.diffViewProvider.scrollToFirstDiff() - if (detectCodeOmission(task.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (detectCodeOmission(task.diffViewProvider.originalContent || "", newContent)) { if (task.diffStrategy) { await task.diffViewProvider.revertChanges() pushToolResult( formatResponse.toolError( - `Content appears to be truncated (file has ${ - newContent.split("\n").length - } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, + `Content appears to contain comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, ), ) return } else { vscode.window .showWarningMessage( - "Potential code truncation detected. cline happens when the AI reaches its max output limit.", - "Follow cline guide to fix the issue", + "Potential code truncation detected. This happens when the AI reaches its max output limit.", + "Follow guide to fix the issue", ) .then((selection) => { - if (selection === "Follow cline guide to fix the issue") { + if (selection === "Follow guide to fix the issue") { vscode.env.openExternal( vscode.Uri.parse( "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 62ec037242..fffa1f07fd 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -36,9 +36,6 @@ vi.mock("../../prompts/responses", () => ({ formatResponse: { toolError: vi.fn((msg) => `Error: ${msg}`), rooIgnoreError: vi.fn((path) => `Access denied: ${path}`), - lineCountTruncationError: vi.fn( - (count, isNew, diffEnabled) => `Line count error: ${count}, new: ${isNew}, diff: ${diffEnabled}`, - ), createPrettyPatch: vi.fn(() => "mock-diff"), }, })) @@ -235,7 +232,6 @@ describe("writeToFileTool", () => { params: { path: testFilePath, content: testContent, - line_count: "3", ...params, }, partial: isPartial, @@ -394,8 +390,9 @@ describe("writeToFileTool", () => { expect(mockedIsPathOutsideWorkspace).toHaveBeenCalled() }) - it("processes files with very large line counts", async () => { - await executeWriteFileTool({ line_count: "999999" }) + it("processes files with large content", async () => { + const largeContent = "Line\n".repeat(10000) + await executeWriteFileTool({ content: largeContent }) // Should process normally without issues expect(mockCline.consecutiveMistakeCount).toBe(0) diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index 335a4330c3..02c077fb36 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -99,6 +99,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web .getConfiguration(Package.name) .get("newTaskRequireTodos", false), toolProtocol, + isStealthModel: modelInfo?.isStealthModel, }, ) diff --git a/src/integrations/editor/__tests__/detect-omission.spec.ts b/src/integrations/editor/__tests__/detect-omission.spec.ts index 3f0ffceb7d..6ff31c390a 100644 --- a/src/integrations/editor/__tests__/detect-omission.spec.ts +++ b/src/integrations/editor/__tests__/detect-omission.spec.ts @@ -8,7 +8,8 @@ describe("detectCodeOmission", () => { return x + y; }` - const generateLongContent = (commentLine: string, length: number = 90) => { + // Generate content with a specified number of lines (100+ lines triggers detection) + const generateLongContent = (commentLine: string, length: number = 110) => { return `${commentLine} ${Array.from({ length }, (_, i) => `const x${i} = ${i};`).join("\n")} const y = 2;` @@ -17,126 +18,63 @@ describe("detectCodeOmission", () => { it("should skip comment checks for files under 100 lines", () => { const newContent = `// Lines 1-50 remain unchanged const z = 3;` - const predictedLineCount = 50 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(false) }) it("should not detect regular comments without omission keywords", () => { const newContent = generateLongContent("// Adding new functionality") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(false) }) it("should not detect when comment is part of original content", () => { const originalWithComment = `// Content remains unchanged ${originalContent}` const newContent = generateLongContent("// Content remains unchanged") - const predictedLineCount = 150 - expect(detectCodeOmission(originalWithComment, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalWithComment, newContent)).toBe(false) }) it("should not detect code that happens to contain omission keywords", () => { const newContent = generateLongContent(`const remains = 'some value'; const unchanged = true;`) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(false) }) - it("should detect suspicious single-line comment when content is more than 20% shorter", () => { + it("should detect suspicious single-line comment for files with 100+ lines", () => { const newContent = generateLongContent("// Previous content remains here\nconst x = 1;") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should not flag suspicious single-line comment when content is less than 20% shorter", () => { - const newContent = generateLongContent("// Previous content remains here", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) - }) - - it("should detect suspicious Python-style comment when content is more than 20% shorter", () => { + it("should detect suspicious Python-style comment for files with 100+ lines", () => { const newContent = generateLongContent("# Previous content remains here\nconst x = 1;") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should not flag suspicious Python-style comment when content is less than 20% shorter", () => { - const newContent = generateLongContent("# Previous content remains here", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) - }) - - it("should detect suspicious multi-line comment when content is more than 20% shorter", () => { + it("should detect suspicious multi-line comment for files with 100+ lines", () => { const newContent = generateLongContent("/* Previous content remains the same */\nconst x = 1;") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) - }) - - it("should not flag suspicious multi-line comment when content is less than 20% shorter", () => { - const newContent = generateLongContent("/* Previous content remains the same */", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should detect suspicious JSX comment when content is more than 20% shorter", () => { + it("should detect suspicious JSX comment for files with 100+ lines", () => { const newContent = generateLongContent("{/* Rest of the code remains the same */}\nconst x = 1;") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) - }) - - it("should not flag suspicious JSX comment when content is less than 20% shorter", () => { - const newContent = generateLongContent("{/* Rest of the code remains the same */}", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should detect suspicious HTML comment when content is more than 20% shorter", () => { + it("should detect suspicious HTML comment for files with 100+ lines", () => { const newContent = generateLongContent("\nconst x = 1;") - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) - }) - - it("should not flag suspicious HTML comment when content is less than 20% shorter", () => { - const newContent = generateLongContent("", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should detect suspicious square bracket notation when content is more than 20% shorter", () => { + it("should detect suspicious square bracket notation for files with 100+ lines", () => { const newContent = generateLongContent( "[Previous content from line 1-305 remains exactly the same]\nconst x = 1;", ) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true) + expect(detectCodeOmission(originalContent, newContent)).toBe(true) }) - it("should not flag suspicious square bracket notation when content is less than 20% shorter", () => { - const newContent = generateLongContent("[Previous content from line 1-305 remains exactly the same]", 130) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) - }) - - it("should not flag content very close to predicted length", () => { - const newContent = generateLongContent( - `const x = 1; -const y = 2; -// This is a legitimate comment that remains here`, - 130, - ) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) - }) - - it("should not flag when content is longer than predicted", () => { - const newContent = generateLongContent( - `const x = 1; -const y = 2; -// Previous content remains here but we added more -const z = 3; -const w = 4;`, - 160, - ) - const predictedLineCount = 150 - expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false) + it("should not flag legitimate comments in files with 100+ lines when in original", () => { + const originalWithComment = `// This is a legitimate comment that remains here +${originalContent}` + const newContent = generateLongContent("// This is a legitimate comment that remains here") + expect(detectCodeOmission(originalWithComment, newContent)).toBe(false) }) }) diff --git a/src/integrations/editor/detect-omission.ts b/src/integrations/editor/detect-omission.ts index 50bef62281..d55acd4183 100644 --- a/src/integrations/editor/detect-omission.ts +++ b/src/integrations/editor/detect-omission.ts @@ -1,23 +1,18 @@ /** * Detects potential AI-generated code omissions in the given file content. + * Looks for comments containing omission keywords that weren't in the original file. * @param originalFileContent The original content of the file. * @param newFileContent The new content of the file to check. - * @param predictedLineCount The predicted number of lines in the new content. * @returns True if a potential omission is detected, false otherwise. */ -export function detectCodeOmission( - originalFileContent: string, - newFileContent: string, - predictedLineCount: number, -): boolean { - // Skip all checks if predictedLineCount is less than 100 - if (!predictedLineCount || predictedLineCount < 100) { +export function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean { + const actualLineCount = newFileContent.split("\n").length + + // Skip checks for small files (less than 100 lines) + if (actualLineCount < 100) { return false } - const actualLineCount = newFileContent.split("\n").length - const lengthRatio = actualLineCount / predictedLineCount - const originalLines = originalFileContent.split("\n") const newLines = newFileContent.split("\n") const omissionKeywords = [ @@ -48,10 +43,7 @@ export function detectCodeOmission( const words = line.toLowerCase().split(/\s+/) if (omissionKeywords.some((keyword) => words.includes(keyword))) { if (!originalLines.includes(line)) { - // For files with 100+ lines, only flag if content is more than 20% shorter - if (lengthRatio <= 0.8) { - return true - } + return true } } } diff --git a/src/shared/tools.ts b/src/shared/tools.ts index e993c81dd9..b754bb3614 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -39,7 +39,6 @@ export const toolParamNames = [ "command", "path", "content", - "line_count", "regex", "file_pattern", "recursive", @@ -106,7 +105,7 @@ export type NativeToolArgs = { switch_mode: { mode_slug: string; reason: string } update_todo_list: { todos: string } use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } - write_to_file: { path: string; content: string; line_count: number } + write_to_file: { path: string; content: string } // Add more tools as they are migrated to native protocol } @@ -164,7 +163,7 @@ export interface FetchInstructionsToolUse extends ToolUse<"fetch_instructions"> export interface WriteToFileToolUse extends ToolUse<"write_to_file"> { name: "write_to_file" - params: Partial, "path" | "content" | "line_count">> + params: Partial, "path" | "content">> } export interface InsertCodeBlockToolUse extends ToolUse<"insert_content"> { diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 355bb35f21..8a18b66fdb 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useRef, useState, useMemo } from "react" import { useEvent } from "react-use" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -// import posthog from "posthog-js" import { ExtensionMessage } from "@roo/ExtensionMessage" import TranslationProvider from "./i18n/TranslationContext" @@ -15,14 +14,8 @@ import { ExtensionStateContextProvider, useExtensionState } from "./context/Exte import ChatView, { ChatViewRef } from "./components/chat/ChatView" import HistoryView from "./components/history/HistoryView" import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView" -import WelcomeView from "./components/welcome/WelcomeView" -// import WelcomeViewProvider from "./components/welcome/WelcomeViewProvider" -// import McpView from "./components/mcp/McpView" -// import { MarketplaceView } from "./components/marketplace/MarketplaceView" -// import ModesView from "./components/modes/ModesView" import CodeReviewPage from "./components/code-review" -// import WelcomeViewProvider from "./components/welcome/WelcomeViewProvider" -// import { MarketplaceView } from "./components/marketplace/MarketplaceView" +import WelcomeView from "./components/welcome/WelcomeViewProvider" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" @@ -106,21 +99,6 @@ const App = () => { } = useExtensionState() const { t } = useTranslation() - // const [useProviderSignupView, setUseProviderSignupView] = useState(false) - - // // Check PostHog feature flag for provider signup view - // // Wait for telemetry to be initialized before checking feature flags - // useEffect(() => { - // if (!didHydrateState || telemetrySetting === "disabled") { - // return - // } - - // posthog.onFeatureFlags(function () { - // // Feature flag for new provider-focused welcome view - // setUseProviderSignupView(posthog?.getFeatureFlag("welcome-provider-signup") === "test") - // }) - // }, [didHydrateState, telemetrySetting]) - // Create a persistent state manager // const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), []) diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index d550c62ef1..4580b6c345 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -5,14 +5,6 @@ import { render, screen, act, cleanup } from "@/utils/test-utils" import AppWithProviders from "../App" -// Mock posthog -vi.mock("posthog-js", () => ({ - default: { - onFeatureFlags: vi.fn(), - getFeatureFlag: vi.fn(), - }, -})) - vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vi.fn(), @@ -307,6 +299,12 @@ describe("App", () => { // marketplaceView.click() // }) + // const marketplaceView = await screen.findByTestId("marketplace-view") + + // act(() => { + // marketplaceView.click() + // }) + // const chatView = screen.getByTestId("chat-view") // expect(chatView.getAttribute("data-hidden")).toBe("false") // expect(screen.queryByTestId("marketplace-view")).not.toBeInTheDocument() diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index b3056d5f38..514ffc902c 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1581,7 +1581,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction} onClick={() => openUpsell()} dismissOnClick={false} - className="bg-none mt-6 border-border rounded-xl p-0 py-3 !text-base"> + className="bg-none mt-6 border-border rounded-xl p-0 py-3 !text-base"> opt.value === "openrouter") + const zgsmIndex = options.findIndex((opt) => opt.value === "zgsm") + if (openRouterIndex > 0) { + options.splice(zgsmIndex, 1) + } if (openRouterIndex > 0) { const [openRouterOption] = options.splice(openRouterIndex, 1) options.unshift(openRouterOption) @@ -564,7 +568,7 @@ const ApiOptions = ({
{errorMessage && } - {selectedProvider === "zgsm" && ( + {!fromWelcomeView && selectedProvider === "zgsm" && ( { - const { - apiConfiguration, - currentApiConfigName, - setApiConfiguration, - uriScheme, - machineId, - useZgsmCustomConfig, - setUseZgsmCustomConfig, - } = useExtensionState() - const { t } = useAppTranslation() - const [errorMessage, setErrorMessage] = useState(undefined) - // const [showRooProvider, setShowRooProvider] = useState(false) - - // // Check PostHog feature flag for Roo provider - // useEffect(() => { - // posthog.onFeatureFlags(function () { - // setShowRooProvider(posthog?.getFeatureFlag("roo-provider-featured") === "test") - // }) - // }, []) - - // Memoize the setApiConfigurationField function to pass to ApiOptions - const setApiConfigurationFieldForApiOptions = useCallback( - (field: K, value: ProviderSettings[K]) => { - setApiConfiguration({ [field]: value }) - }, - [setApiConfiguration], // setApiConfiguration from context is stable - ) - - const handleSubmit = useCallback(() => { - const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined - - if (error) { - setErrorMessage(error) - return - } - - setErrorMessage(undefined) - vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) - }, [apiConfiguration, currentApiConfigName]) - - const handleVisitCloudWebsite = useCallback(() => { - const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined - - if (error) { - setErrorMessage(error) - return - } - - setErrorMessage(undefined) - - // Send telemetry for account connect action - telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) - - vscode.postMessage({ type: "zgsmLogin", apiConfiguration }) - }, [apiConfiguration]) - - // Using a lazy initializer so it reads once at mount - const [imagesBaseUri] = useState(() => { - const w = window as any - return w.IMAGES_BASE_URI || "" - }) - - return ( - - - - {/*

{t("welcome:greeting")}

*/} - -
-

- -

-

- -

-
- -
- {apiConfiguration?.apiProvider !== "zgsm" && ( - <> -

{t("welcome:startRouter")}

- -
- {/* Define the providers */} - {(() => { - // Provider card configuration - const baseProviders = [ - { - slug: "requesty", - name: "Requesty", - description: t("welcome:routers.requesty.description"), - incentive: t("welcome:routers.requesty.incentive"), - authUrl: getRequestyAuthUrl(uriScheme), - }, - { - slug: "openrouter", - name: "OpenRouter", - description: t("welcome:routers.openrouter.description"), - authUrl: getOpenRouterAuthUrl(uriScheme), - }, - ] - - // Conditionally add Roo provider based on feature flag - // const providers = showRooProvider - // ? [ - // ...baseProviders, - // { - // slug: "roo", - // name: "Roo Code Cloud", - // description: t("welcome:routers.roo.description"), - // incentive: t("welcome:routers.roo.incentive"), - // authUrl: "#", // Placeholder since onClick handler will prevent default - // }, - // ] - // : baseProviders - - // Shuffle providers based on machine ID (will be consistent for the same machine) - // const orderedProviders = [...providers] - knuthShuffle(baseProviders, (machineId as any) || Date.now()) - - // Render the provider cards - return baseProviders.map((provider, index) => ( - { - // Track telemetry for featured provider click - telemetryClient.capture(TelemetryEventName.FEATURED_PROVIDER_CLICKED, { - provider: provider.slug, - }) - - // // Special handling for Roo provider - // if (provider.slug === "roo") { - // e.preventDefault() - - // // Set the Roo provider configuration - // const rooConfig: ProviderSettings = { - // apiProvider: "roo", - // } - - // // Save the Roo provider configuration - // vscode.postMessage({ - // type: "upsertApiConfiguration", - // text: currentApiConfigName, - // apiConfiguration: rooConfig, - // }) - - // // Then trigger cloud sign-in - // vscode.postMessage({ type: "rooCloudSignIn" }) - // } - // // For other providers, let the default link behavior work - }}> - {provider.incentive && ( -
- {provider.incentive} -
- )} -
- {provider.name} -
-
-
- {provider.name} -
-
- {provider.description} -
-
-
- )) - })()} -
- -

{t("welcome:startCustom")}

- - )} - setUseZgsmCustomConfig(value ?? false)} - /> -
-
-
-
-
- { - e.preventDefault() - vscode.postMessage({ type: "importSettings" }) - }} - className="text-sm"> - {t("welcome:importSettings")} - -
- {apiConfiguration?.apiProvider === "zgsm" ? ( - - ) : ( - - )} - {errorMessage &&
{errorMessage}
} -
-
-
- ) -} - -export default WelcomeView diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index a631b74096..011fdc8a25 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -1,16 +1,12 @@ -import { useCallback, useEffect, useRef, useState } from "react" -import { - VSCodeLink, - VSCodeProgressRing, - VSCodeRadio, - VSCodeRadioGroup, - VSCodeTextField, -} from "@vscode/webview-ui-toolkit/react" +import { useCallback, useState } from "react" +import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { telemetryClient } from "@src/utils/TelemetryClient" import type { ProviderSettings } from "@roo-code/types" +import { TelemetryEventName } from "@roo-code/types" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { validateApiConfiguration } from "@src/utils/validate" +import { isValidUrl, validateApiConfiguration, validateZgsmBaseUrl } from "@src/utils/validate" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" import { Button } from "@src/components/ui" @@ -20,9 +16,10 @@ import { Tab, TabContent } from "../common/Tab" import RooHero from "./RooHero" import { Trans } from "react-i18next" -import { ArrowLeft, ArrowRight, BadgeInfo } from "lucide-react" +import { inputEventTransform } from "../settings/transforms" +import { ApiErrorMessage } from "../settings/ApiErrorMessage" -type ProviderOption = "roo" | "custom" +type ProviderOption = "zgsm" | "custom" const WelcomeViewProvider = () => { const { @@ -32,45 +29,11 @@ const WelcomeViewProvider = () => { uriScheme, useZgsmCustomConfig, setUseZgsmCustomConfig, - cloudIsAuthenticated, } = useExtensionState() - // const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, cloudIsAuthenticated } = - useExtensionState() const { t } = useAppTranslation() const [errorMessage, setErrorMessage] = useState(undefined) - const [selectedProvider, setSelectedProvider] = useState("roo") - const [authInProgress, setAuthInProgress] = useState(false) - const [showManualEntry, setShowManualEntry] = useState(false) - const [manualUrl, setManualUrl] = useState("") - const [manualErrorMessage, setManualErrorMessage] = useState(undefined) - const manualUrlInputRef = useRef(null) - - // When auth completes during the provider signup flow, save the Roo config - // This will cause showWelcome to become false and navigate to chat - useEffect(() => { - if (cloudIsAuthenticated && authInProgress) { - // Auth completed from provider signup flow - save the config now - const rooConfig: ProviderSettings = { - apiProvider: "roo", - } - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentApiConfigName, - apiConfiguration: rooConfig, - }) - setAuthInProgress(false) - setShowManualEntry(false) - } - }, [cloudIsAuthenticated, authInProgress, currentApiConfigName]) - - // Focus the manual URL input when it becomes visible - useEffect(() => { - if (showManualEntry && manualUrlInputRef.current) { - setTimeout(() => { - manualUrlInputRef.current?.focus() - }, 50) - } - }, [showManualEntry]) + const [selectedProvider, setSelectedProvider] = useState("zgsm") + const [costrictBaseurl, setCostrictBaseurl] = useState("") // Memoize the setApiConfigurationField function to pass to ApiOptions const setApiConfigurationFieldForApiOptions = useCallback( @@ -81,232 +44,161 @@ const WelcomeViewProvider = () => { ) const handleGetStarted = useCallback(() => { - if (selectedProvider === "roo") { - // Trigger cloud sign-in with provider signup flow - // NOTE: We intentionally do NOT save the API configuration yet. - // The configuration will be saved by the extension after auth completes. - // This keeps showWelcome true so we can show the waiting state. - vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: true }) - - // Show the waiting state - setAuthInProgress(true) + let error = undefined + if (apiConfiguration.apiProvider === "zgsm") { + error = t("settings:providers.noProviderMatchFound") } else { // Use custom provider - validate first - const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined - - if (error) { - setErrorMessage(error) - return - } - - setErrorMessage(undefined) - vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) + error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined } - }, [selectedProvider, apiConfiguration, currentApiConfigName]) - const handleGoBack = useCallback(() => { - setAuthInProgress(false) - setShowManualEntry(false) - setManualUrl("") - setManualErrorMessage(false) - }, []) - - const handleManualUrlChange = (e: any) => { - const url = e.target.value - setManualUrl(url) + if (error) { + setErrorMessage(error) + return + } - // Auto-trigger authentication when a complete URL is pasted - setTimeout(() => { - if (url.trim() && url.includes("://") && url.includes("/auth/clerk/callback")) { - setManualErrorMessage(false) - vscode.postMessage({ type: "rooCloudManualUrl", text: url.trim() }) - } - }, 100) - } + setErrorMessage(undefined) + vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) + }, [apiConfiguration, currentApiConfigName, t]) - const handleSubmit = useCallback(() => { - const url = manualUrl.trim() - if (url && url.includes("://") && url.includes("/auth/clerk/callback")) { - setManualErrorMessage(false) - vscode.postMessage({ type: "rooCloudManualUrl", text: url }) - } else { - setManualErrorMessage(true) - } - }, [manualUrl]) + const handleInputChange = useCallback((e: any) => { + e?.preventDefault() - const handleOpenSignupUrl = () => { - vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: true }) - } + const val = inputEventTransform(e) - // Render the waiting for cloud state - if (authInProgress) { - return ( - - -
- -

{t("welcome:waitingForCloud.heading")}

-

- {t("welcome:waitingForCloud.description")} -

+ setCostrictBaseurl(val) + }, []) -
- -

- - ), - }} - /> -

-
+ const handleVisitCloudWebsite = useCallback(() => { + const error = validateZgsmBaseUrl(costrictBaseurl) -
- -
-

- setShowManualEntry(true)} - className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline cursor-pointer bg-transparent border-none p-0 " - /> - ), - }} - /> -

+ if (error) { + setErrorMessage(error) + return + } - {showManualEntry && ( -
-

- {t("welcome:waitingForCloud.pasteUrl")} -

-
- - -
- {manualUrl && manualErrorMessage && ( -

- {t("welcome:waitingForCloud.invalidURL")} -

- )} -
- )} -
-
-
+ setErrorMessage(undefined) -
- -
-
-
- ) - } + // Send telemetry for account connect action + telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) + if (isValidUrl(costrictBaseurl)) { + setApiConfigurationFieldForApiOptions("zgsmBaseUrl", costrictBaseurl.trim().replace(/\/$/, "")) + } + vscode.postMessage({ + type: "zgsmLogin", + apiConfiguration: { + ...apiConfiguration, + apiProvider: "zgsm", + zgsmModelId: "Auto", + }, + }) + }, [apiConfiguration, costrictBaseurl, setApiConfigurationFieldForApiOptions]) return ( -

{t("welcome:greeting")}

+ {/*

{t("welcome:greeting")}

*/}
- {selectedProvider === "roo" && ( + {selectedProvider === "zgsm" && (

)}

+ { + e.preventDefault() + vscode.postMessage({ type: "importSettings" }) + }} + className="text-sm"> + {t("welcome:importSettings")} +

) => { + setErrorMessage(undefined) const target = ((e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)) as HTMLInputElement setSelectedProvider(target.value as ProviderOption) }}> - {/* Roo Code Cloud Provider Option */} - -
+ +

- {t("welcome:providerSignup.rooCloudProvider")} -

-

- {t("welcome:providerSignup.rooCloudDescription")} ( - + {t("welcome:providerSignup.rooCloudProvider")}  + {t("welcome:providerSignup.learnMore")} - ).

- - {/* Use Another Provider Option */} - -
+ +

{t("welcome:providerSignup.useAnotherProvider")}

-

+

{t("welcome:providerSignup.useAnotherProviderDescription")}

- {/* Expand API options only when custom provider is selected, max height is used to force a transition */} -
-
-

- {t("welcome:providerSignup.noApiKeys")} -

- setUseZgsmCustomConfig(value ?? false)} - /> -
+
+ {selectedProvider === "custom" ? ( +
+

+ {t("welcome:providerSignup.noApiKeys")} +

+ setUseZgsmCustomConfig(value ?? false)} + /> +
+ ) : ( +
+ + + + {errorMessage && } +
+ )}
-
- + {selectedProvider === "zgsm" ? ( + + ) : ( + + )}
diff --git a/webview-ui/src/i18n/costrict-i18n/locales/en/welcome.json b/webview-ui/src/i18n/costrict-i18n/locales/en/welcome.json index 1ec175e26b..779341e7d4 100644 --- a/webview-ui/src/i18n/costrict-i18n/locales/en/welcome.json +++ b/webview-ui/src/i18n/costrict-i18n/locales/en/welcome.json @@ -6,7 +6,7 @@ "title": "Help Improve CoStrict", "anonymousTelemetry": "Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or personal information is ever sent (unless you connect to CoStrict). See our privacy policy for more details." }, - "chooseProvider": "To do its magic, CoStrict needs an API key.", + "chooseProvider": "CoStrict needs an LLM provider to work. Choose yours:", "vibe": { "description": "Vibe coding alternates between clarifying requirements and generating code. It is ideal for rapid development and simple tasks, and empowers developers with strong prompt engineering skills to efficiently produce high-quality code." }, @@ -14,5 +14,14 @@ "description": "Strict coding workflow: clarify requirements → design → task decomposition → test, self-verify, self-repair. Ideal for complex tasks (High token consumption)." }, "developmentMode": "Development Mode", - "commonFeatures": "Common Features" + "commonFeatures": "Common Features", + "providerSignup": { + "rooCloudProvider": "CoStrict Provider", + "rooCloudDescription": "The simplest way to use CoStrict. A curated mix of free and paid models at a low cost", + "learnMore": "learn more", + "useAnotherProvider": "Use another provider", + "useAnotherProviderDescription": "Enter an API key and get going.", + "noApiKeys": "Don't want to deal with keys? Go with the CoStrict Provider.", + "getStarted": "Get started" + } } diff --git a/webview-ui/src/i18n/costrict-i18n/locales/zh-CN/welcome.json b/webview-ui/src/i18n/costrict-i18n/locales/zh-CN/welcome.json index c699198000..0d72e7e4c1 100644 --- a/webview-ui/src/i18n/costrict-i18n/locales/zh-CN/welcome.json +++ b/webview-ui/src/i18n/costrict-i18n/locales/zh-CN/welcome.json @@ -6,7 +6,7 @@ "title": "帮助改进 CoStrict", "anonymousTelemetry": "发送匿名的错误和使用数据,以帮助我们修复错误并改进扩展程序。不会涉及代码、提示词或个人隐私信息。" }, - "chooseProvider": "CoStrict 需要一个 API 密钥才能发挥魔力。", + "chooseProvider": "开始使用需要 LLM 提供商:", "vibe": { "description": "氛围编程,需求澄清与代码生成交替进行。适配快速开发或简单任务,若使用者具备较强提示词能力,可高效产出高质量代码。" }, @@ -14,5 +14,14 @@ "description": "严肃编程,遵从「需求澄清→设计→任务拆解→测试自校验自修复」流程。适配复杂任务 (Token消耗量大)。" }, "developmentMode": "开发模式", - "commonFeatures": "常用功能" + "commonFeatures": "常用功能", + "providerSignup": { + "rooCloudProvider": "CoStrict 提供商", + "rooCloudDescription": "使用 CoStrict 最简单的方式。低成本精选的免费和付费模型组合", + "learnMore": "了解更多", + "useAnotherProvider": "使用其他提供商", + "useAnotherProviderDescription": "输入 API 密钥即可开始。", + "noApiKeys": "不想处理密钥?选择 CoStrict 提供商吧。", + "getStarted": "开始使用" + } } diff --git a/webview-ui/src/i18n/costrict-i18n/locales/zh-TW/welcome.json b/webview-ui/src/i18n/costrict-i18n/locales/zh-TW/welcome.json index 2789115c04..cb269e6b9f 100644 --- a/webview-ui/src/i18n/costrict-i18n/locales/zh-TW/welcome.json +++ b/webview-ui/src/i18n/costrict-i18n/locales/zh-TW/welcome.json @@ -6,7 +6,7 @@ "title": "幫助改進 CoStrict", "anonymousTelemetry": "傳送匿名錯誤和使用資料,以協助我們修復錯誤並改善擴充功能。我們絕不會傳送任何程式碼、命令提示詞、或個人資訊 (除非您使用了 CoStrict)。詳細資訊請參閱我們的隱私權政策。" }, - "chooseProvider": "CoStrict 需要一個 API 金鑰才能發揮魔力。", + "chooseProvider": "開始使用需要 LLM 提供者:", "vibe": { "description": "氛圍編程,輕量交互模式,需求澄清與代碼生成交替進行。適配快速開發或簡單任務,若使用者具備較強提示詞能力,可高效產出高質量代碼。" }, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 4d0b623965..5c04ebe926 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -39,7 +39,7 @@ export function validateApiConfiguration( function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): string | undefined { switch (apiConfiguration.apiProvider) { case "zgsm": - return validateZgsmBaseUrl(apiConfiguration) + return validateZgsmBaseUrl(apiConfiguration.zgsmBaseUrl) case "openrouter": if (!apiConfiguration.openRouterApiKey) { return i18next.t("settings:validation.apiKey") @@ -357,8 +357,8 @@ export const isValidUrl = (url: string) => { } } -export function validateZgsmBaseUrl(apiConfiguration: ProviderSettings): string | undefined { - const zgsmBaseUrl = apiConfiguration.zgsmBaseUrl?.trim() +export function validateZgsmBaseUrl(zgsmBaseUrl?: string): string | undefined { + zgsmBaseUrl = zgsmBaseUrl?.trim() if (!zgsmBaseUrl || isValidUrl(zgsmBaseUrl)) { return undefined }