fix: internationalize token stats tooltip and add tooltip for API Request without price

- Added i18n translations for "Input" and "Output" in chat.json
- Updated ChatRow.tsx to use translated strings in token stats tooltip
- Added tooltip to "API Request" text when there are token stats but no price
- Ensures token information is always accessible via tooltip hover
This commit is contained in:
Roo Code 2025-09-12 01:57:49 +00:00
parent 08d7f80e22
commit ef3fd5b20e
202 changed files with 2270 additions and 6356 deletions

View file

@ -30,13 +30,12 @@
<tools>
<tool>gh pr checkout [PR_NUMBER] --force</tool>
<tool>git fetch origin main</tool>
<tool>GIT_EDITOR=true git rebase origin/main</tool>
<tool>git rebase origin/main</tool>
</tools>
<details>
Force checkout the PR branch to ensure clean state
Fetch the latest main branch
Attempt to rebase onto main to reveal conflicts
Use GIT_EDITOR=true to ensure non-interactive rebase
</details>
</step>
@ -109,8 +108,8 @@
</command>
<command name="rebase_main">
<syntax>GIT_EDITOR=true git rebase origin/main</syntax>
<purpose>Rebase current branch onto main to reveal conflicts (non-interactive)</purpose>
<syntax>git rebase origin/main</syntax>
<purpose>Rebase current branch onto main to reveal conflicts</purpose>
</command>
<command name="get_blame_info">
@ -134,20 +133,6 @@
</command>
</git_commands>
<command name="continue_rebase">
<syntax>GIT_EDITOR=true git rebase --continue</syntax>
<purpose>Continue rebase after resolving conflicts (non-interactive)</purpose>
</command>
</git_commands>
<environment_variables>
<variable name="GIT_EDITOR">
<value>true</value>
<purpose>Set to 'true' (a no-op command) to prevent interactive prompts during rebase operations</purpose>
<usage>Prefix git rebase commands with GIT_EDITOR=true to ensure non-interactive execution</usage>
</variable>
</environment_variables>
<completion_criteria>
<criterion>All merge conflicts have been resolved</criterion>
<criterion>Resolved files have been staged</criterion>

View file

@ -26,8 +26,6 @@
<practice>Chain git commands with && for efficiency</practice>
<practice>Use --format options for structured output</practice>
<practice>Capture command output for parsing</practice>
<practice>Use GIT_EDITOR=true for non-interactive git rebase operations</practice>
<practice>Set environment variables inline to avoid prompts during automation</practice>
</best_practices>
<common_commands>
@ -48,7 +46,7 @@
<command>
<purpose>Rebase onto main to reveal conflicts</purpose>
<syntax>GIT_EDITOR=true git rebase origin/main</syntax>
<syntax>git rebase origin/main</syntax>
</command>
<command>
@ -73,7 +71,7 @@
<command>
<purpose>Continue rebase after resolution</purpose>
<syntax>GIT_EDITOR=true git rebase --continue</syntax>
<syntax>git rebase --continue</syntax>
</command>
</common_commands>
</tool>
@ -154,7 +152,7 @@ const config = {
<step>execute_command - Get PR info with gh CLI</step>
<step>execute_command - Checkout PR with gh pr checkout --force</step>
<step>execute_command - Fetch origin main</step>
<step>execute_command - Rebase onto origin/main with GIT_EDITOR=true</step>
<step>execute_command - Rebase onto origin/main</step>
<step>execute_command - Check for conflicts with git status</step>
</sequence>
</pattern>
@ -180,22 +178,13 @@ const config = {
<pattern name="complete_rebase">
<sequence>
<step>execute_command - Check all conflicts resolved</step>
<step>execute_command - Continue rebase with GIT_EDITOR=true git rebase --continue</step>
<step>execute_command - Continue rebase with git rebase --continue</step>
<step>execute_command - Verify clean status</step>
</sequence>
</pattern>
</tool_combination_patterns>
<error_handling>
<scenario name="interactive_prompt_blocking">
<description>Git commands waiting for interactive input</description>
<approach>
Use GIT_EDITOR=true to bypass editor prompts
Set GIT_SEQUENCE_EDITOR=true for sequence editing
Consider --no-edit flag for commit operations
</approach>
</scenario>
<scenario name="no_conflicts_after_rebase">
<description>Rebase completes without conflicts</description>
<approach>
@ -236,42 +225,4 @@ const config = {
</approach>
</scenario>
</error_handling>
<non_interactive_operations>
<overview>
Ensuring git operations run without requiring user interaction is critical
for automated conflict resolution. The mode uses environment variables to
bypass interactive prompts.
</overview>
<techniques>
<technique name="GIT_EDITOR">
<description>Set to 'true' (a no-op command) to skip editor prompts</description>
<usage>GIT_EDITOR=true git rebase --continue</usage>
<when>During rebase operations that would normally open an editor</when>
</technique>
<technique name="GIT_SEQUENCE_EDITOR">
<description>Skip interactive rebase todo editing</description>
<usage>GIT_SEQUENCE_EDITOR=true git rebase -i HEAD~3</usage>
<when>When interactive rebase is triggered but no editing needed</when>
</technique>
<technique name="commit_flags">
<description>Use flags to avoid interactive prompts</description>
<examples>
<example>git commit --no-edit (use existing message)</example>
<example>git merge --no-edit (skip merge message editing)</example>
<example>git cherry-pick --no-edit (keep original message)</example>
</examples>
</technique>
</techniques>
<best_practices>
<practice>Always test commands locally first to identify potential prompts</practice>
<practice>Combine environment variables when multiple editors might be invoked</practice>
<practice>Document why non-interactive mode is used in comments</practice>
<practice>Have fallback strategies if automation fails</practice>
</best_practices>
</non_interactive_operations>
</merge_resolver_tool_usage>

View file

@ -54,7 +54,7 @@ From github.com:user/repo
<tool_use><![CDATA[
<execute_command>
<command>GIT_EDITOR=true git rebase origin/main</command>
<command>git rebase origin/main</command>
</execute_command>
]]></tool_use>
<expected_output><![CDATA[
@ -251,7 +251,7 @@ abc123 Fix: Add listener cleanup to prevent memory leak
<tool_use><![CDATA[
<execute_command>
<command>GIT_EDITOR=true git rebase --continue</command>
<command>git rebase --continue</command>
</execute_command>
]]></tool_use>
<expected_output><![CDATA[
@ -309,8 +309,7 @@ Both the feature refactor and the critical bugfix have been preserved in the res
<takeaway>Use git blame and commit messages to understand the history</takeaway>
<takeaway>Combine non-conflicting improvements when possible</takeaway>
<takeaway>Prioritize bugfixes while accommodating refactors</takeaway>
<takeaway>Use GIT_EDITOR=true to ensure non-interactive rebase operations</takeaway>
<takeaway>Complete the rebase process with GIT_EDITOR=true git rebase --continue</takeaway>
<takeaway>Complete the rebase process with git rebase --continue</takeaway>
<takeaway>Validate that both sets of changes work together</takeaway>
</key_takeaways>
</merge_resolver_example>

View file

@ -1,40 +1,5 @@
# Roo Code Changelog
## [3.28.1] - 2025-09-11
![3.28.1 Release - Kangaroo riding rocket to the clouds](/releases/3.28.1-release.png)
- Announce Roo Code Cloud!
- Add cloud task button for opening tasks in Roo Code Cloud (thanks @app/roomote!)
- Make Posthog telemetry the default (thanks @mrubens!)
- Show notification when the checkpoint initialization fails (thanks @app/roomote!)
- Bust cache in generated image preview (thanks @mrubens!)
- Fix: Center active mode in selector dropdown on open (#7882 by @hannesrudolph, PR by @app/roomote)
- Fix: Preserve first message during conversation condensing (thanks @daniel-lxs!)
## [3.28.0] - 2025-09-10
![3.28.0 Release - Continue tasks in Roo Code Cloud](/releases/3.28.0-release.png)
- feat: Continue tasks in Roo Code Cloud (thanks @brunobergher!)
- feat: Support connecting to Cloud without redirect handling (thanks @mrubens!)
- feat: Add toggle to control task syncing to Cloud (thanks @jr!)
- feat: Add click-to-edit, ESC-to-cancel, and fix padding consistency for chat messages (#7788 by @hannesrudolph, PR by @app/roomote)
- feat: Make reasoning more visible (thanks @app/roomote!)
- fix: Fix Groq context window display (thanks @mrubens!)
- fix: Add GIT_EDITOR env var to merge-resolver mode for non-interactive rebase (thanks @daniel-lxs!)
- fix: Resolve chat message edit/delete duplication issues (thanks @daniel-lxs!)
- fix: Reduce CodeBlock button z-index to prevent overlap with popovers (#7703 by @A0nameless0man, PR by @daniel-lxs)
- fix: Revert PR #7188 - Restore temperature parameter to fix TabbyApi/ExLlamaV2 crashes (#7581 by @drknyt, PR by @daniel-lxs)
- fix: Make ollama models info transport work like lmstudio (#7674 by @ItsOnlyBinary, PR by @ItsOnlyBinary)
- fix: Update DeepSeek pricing to new unified rates effective Sept 5, 2025 (#7685 by @NaccOll, PR by @app/roomote)
- feat: Update Vertex AI models and regions (#7725 by @ssweens, PR by @ssweens)
- chore: Update dependency eslint-plugin-turbo to v2.5.6 (thanks @app/renovate!)
- chore: Update dependency @changesets/cli to v2.29.6 (thanks @app/renovate!)
- chore: Update dependency nock to v14.0.10 (thanks @app/renovate!)
- chore: Update dependency eslint-config-prettier to v10.1.8 (thanks @app/renovate!)
- chore: Update dependency esbuild to v0.25.9 (thanks @app/renovate!)
## [3.27.0] - 2025-09-05
![3.27.0 Release - Bug Fixes and Improvements](/releases/3.27.0-release.png)

View file

@ -1,6 +1,6 @@
# Roo Code Privacy Policy
**Last Updated: September 11th, 2025**
**Last Updated: August 20th, 2025**
Roo Code respects your privacy and is committed to transparency about how we handle your data. Below is a simple breakdown of where key pieces of data go—and, importantly, where they dont.
@ -10,19 +10,19 @@ Roo Code respects your privacy and is committed to transparency about how we han
- **Commands**: Any commands executed through Roo Code happen on your local environment. However, when you use AI-powered features, the relevant code and context from your commands may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not have access to or store this data, but AI providers may process it per their privacy policies.
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service. If you choose Roo Code Cloud as the provider (proxy mode), prompts may transit Roo Code servers only to forward them to the upstream model and are not stored.
- **API Keys & Credentials**: If you enter an API key (e.g., to connect an AI model), it is stored locally on your device and never sent to us or any third party, except the provider you have chosen.
- **Telemetry (Usage Data)**: We collect anonymous feature usage and error data to help us improve Roo Code. This telemetry is powered by PostHog and includes your VS Code machine ID, feature usage patterns, and exception reports. This telemetry does **not** collect personally identifiable information, your code, or AI prompts. You can opt out of this telemetry at any time through the settings.
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Code's backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.
- **Telemetry (Usage Data)**: We only collect feature usage and error data if you explicitly opt-in. This telemetry is powered by PostHog and helps us understand feature usage to improve Roo Code. This includes your VS Code machine ID and feature usage patterns and exception reports. We do **not** collect personally identifiable information, your code, or AI prompts.
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Codes backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.
### **How We Use Your Data (If Collected)**
- We use telemetry to understand feature usage and improve Roo Code.
- If you opt-in to telemetry, we use it to understand feature usage and improve Roo Code.
- We do **not** sell or share your data.
- We do **not** train any models on your data.
### **Your Choices & Control**
- You can run models locally to prevent data being sent to third-parties.
- Telemetry collection is enabled by default to help us improve Roo Code, but you can opt out at any time through the settings.
- By default, telemetry collection is off and if you turn it on, you can opt out of telemetry at any time.
- You can delete Roo Code to stop all data collection.
### **Security & Updates**

View file

@ -49,6 +49,16 @@ Check out the [CHANGELOG](CHANGELOG.md) for detailed updates and fixes.
---
## 🎉 Roo Code 3.25 Released
Roo Code 3.25 brings powerful new features and significant improvements to enhance your development workflow!
- **Message Queueing** - Queue multiple messages while Roo is working, allowing you to continue planning your workflow without interruption.
- **Custom Slash Commands** - Create personalized slash commands for quick access to frequently used prompts and workflows, with full UI management.
- **Enhanced Gemini Tools** - New URL context and Google Search grounding capabilities provide Gemini models with real-time web information and enhanced research abilities.
---
## What Can Roo Code Do?
- 🚀 **Generate Code** from natural language descriptions

View file

@ -21,7 +21,7 @@ const nextConfig: NextConfig = {
destination: "https://roocode.com/:path*",
permanent: true,
},
// Redirect cloud waitlist to Notion page (kept for extension compatibility)
// Redirect cloud waitlist to Notion page
{
source: "/cloud-waitlist",
destination: "https://roo-code.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",

View file

@ -1,35 +1,61 @@
"use client"
import { useMemo } from "react"
import { ScatterChart, Scatter, XAxis, YAxis, Label, Customized, Cross } from "recharts"
import type { TaskMetrics, Run } from "@roo-code/evals"
import { formatTokens, formatCurrency, formatDuration, formatScore } from "@/lib"
import { useOpenRouterModels } from "@/lib/hooks"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartConfig,
ChartLegend,
ChartLegendContent,
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui"
import type { EvalRun } from "./types"
import { Plot } from "./plot"
export function Evals({ runs }: { runs: EvalRun[] }) {
export function Evals({
runs,
}: {
runs: (Run & {
label: string
score: number
languageScores?: Record<"go" | "java" | "javascript" | "python" | "rust", number>
taskMetrics: TaskMetrics
modelId?: string
})[]
}) {
const { data: openRouterModels } = useOpenRouterModels()
const tableData: (EvalRun & { label: string; cost: number })[] = useMemo(
const tableData = useMemo(
() =>
runs.map((run) => {
const openRouterModelInfo = openRouterModels?.[run.modelId ?? ""]?.modelInfo
return {
...run,
label: run.name || run.description || run.model,
cost: run.taskMetrics.cost,
description: run.description ?? openRouterModelInfo?.description ?? null,
contextWindow: run.contextWindow ?? openRouterModelInfo?.contextWindow ?? null,
inputPrice: run.inputPrice ?? openRouterModelInfo?.inputPrice ?? null,
outputPrice: run.outputPrice ?? openRouterModelInfo?.outputPrice ?? null,
}
}),
runs.map((run) => ({
...run,
label: run.description || run.model,
score: run.score,
cost: run.taskMetrics.cost,
model: openRouterModels?.[run.modelId ?? ""],
modelInfo: openRouterModels?.[run.modelId ?? ""]?.modelInfo,
})),
[runs, openRouterModels],
)
const chartData = useMemo(() => tableData.filter(({ cost }) => cost < 100), [tableData])
const chartConfig = useMemo(
() => chartData.reduce((acc, run) => ({ ...acc, [run.label]: run }), {} as ChartConfig),
[chartData],
)
return (
<div className="mx-auto flex max-w-screen-lg flex-col gap-8 p-8">
<div className="flex flex-col gap-4">
@ -101,15 +127,17 @@ export function Evals({ runs }: { runs: EvalRun[] }) {
<TableBody className="font-mono">
{tableData.map((run) => (
<TableRow key={run.id}>
<TableCell title={run.description ?? undefined}>
<TableCell title={run.model?.description}>
<div className="font-sans">{run.label}</div>
<div className="text-xs opacity-50">{formatTokens(run.contextWindow)}</div>
<div className="text-xs opacity-50">
{formatTokens(run.modelInfo?.contextWindow ?? 0)}
</div>
</TableCell>
<TableCell className="border-r">
<div className="flex flex-row gap-2">
<div>{formatCurrency(run.inputPrice)}</div>
<div>{formatCurrency(run.modelInfo?.inputPrice ?? 0)}</div>
<div className="opacity-25">/</div>
<div>{formatCurrency(run.outputPrice)}</div>
<div>{formatCurrency(run.modelInfo?.outputPrice ?? 0)}</div>
</div>
</TableCell>
<TableCell className="font-mono">{formatDuration(run.taskMetrics.duration)}</TableCell>
@ -141,9 +169,58 @@ export function Evals({ runs }: { runs: EvalRun[] }) {
))}
</TableBody>
<TableCaption>
<Plot tableData={tableData} />
<div className="pb-4 font-medium">Cost Versus Score</div>
<ChartContainer config={chartConfig} className="h-[500px] w-full">
<ScatterChart margin={{ top: 0, right: 0, bottom: 0, left: 20 }}>
<XAxis
type="number"
dataKey="cost"
name="Cost"
domain={[
(dataMin: number) => Math.round((dataMin - 5) / 5) * 5,
(dataMax: number) => Math.round((dataMax + 5) / 5) * 5,
]}
tickFormatter={(value) => formatCurrency(value)}>
<Label value="Cost" position="bottom" offset={0} />
</XAxis>
<YAxis
type="number"
dataKey="score"
name="Score"
domain={[
(dataMin: number) => Math.max(0, Math.round((dataMin - 5) / 5) * 5),
(dataMax: number) => Math.min(100, Math.round((dataMax + 5) / 5) * 5),
]}
tickFormatter={(value) => `${value}%`}>
<Label value="Score" angle={-90} position="left" dy={-15} />
</YAxis>
<ChartTooltip content={<ChartTooltipContent labelKey="label" hideIndicator />} />
<Customized component={renderQuadrant} />
{chartData.map((d, i) => (
<Scatter key={d.label} name={d.label} data={[d]} fill={`hsl(var(--chart-${i + 1}))`} />
))}
<ChartLegend content={<ChartLegendContent />} />
</ScatterChart>
</ChartContainer>
<div className="py-4 text-xs opacity-50">
(Note: Very expensive models are excluded from the scatter plot.)
</div>
</TableCaption>
</Table>
</div>
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderQuadrant = (props: any) => (
<Cross
width={props.width}
height={props.height}
x={props.width / 2 + 35}
y={props.height / 2 - 15}
top={0}
left={0}
stroke="currentColor"
opacity={0.1}
/>
)

View file

@ -1,336 +0,0 @@
"use client"
import { useMemo } from "react"
import { ScatterChart, Scatter, XAxis, YAxis, Customized, Cross, LabelList } from "recharts"
import { formatCurrency } from "@/lib"
import { ChartContainer, ChartTooltip, ChartConfig } from "@/components/ui"
import type { EvalRun } from "./types"
type PlotProps = {
tableData: (EvalRun & { label: string; cost: number })[]
}
type LabelPosition = "top" | "bottom" | "left" | "right"
export const Plot = ({ tableData }: PlotProps) => {
const chartData = useMemo(() => tableData.filter(({ cost }) => cost < 50), [tableData])
const chartConfig = useMemo(
() => chartData.reduce((acc, run) => ({ ...acc, [run.label]: run }), {} as ChartConfig),
[chartData],
)
// Calculate label positions to avoid overlaps.
const labelPositions = useMemo(() => {
const positions: Record<string, LabelPosition> = {}
// Track placed labels with their approximate bounds.
const placedLabels: Array<{
cost: number
score: number
label: string
position: LabelPosition
}> = []
// Helper function to check if two labels would overlap.
const wouldLabelsOverlap = (
p1: { cost: number; score: number; position: LabelPosition },
p2: { cost: number; score: number; position: LabelPosition },
): boolean => {
// Approximate thresholds for overlap detection.
const horizontalThreshold = 4 // Cost units.
const verticalThreshold = 5 // Score units.
const costDiff = Math.abs(p1.cost - p2.cost)
const scoreDiff = Math.abs(p1.score - p2.score)
// If points are far apart, no overlap.
if (costDiff > horizontalThreshold * 2 || scoreDiff > verticalThreshold * 2) {
return false
}
// Check specific position combinations for overlap.
// Same position for nearby points definitely overlaps.
if (p1.position === p2.position && costDiff < horizontalThreshold && scoreDiff < verticalThreshold) {
return true
}
// Check adjacent position overlaps.
const p1IsTop = p1.position === "top"
const p1IsBottom = p1.position === "bottom"
const p2IsTop = p2.position === "top"
const p2IsBottom = p2.position === "bottom"
// If both labels are on the same vertical side and points are close
// horizontally.
if ((p1IsTop && p2IsTop) || (p1IsBottom && p2IsBottom)) {
if (costDiff < horizontalThreshold && scoreDiff < verticalThreshold / 2) {
return true
}
}
return false
}
// Helper function to check if position would overlap with a data point.
const wouldOverlapPoint = (point: (typeof chartData)[0], position: LabelPosition): boolean => {
for (const other of chartData) {
if (other.label === point.label) {
continue
}
const costDiff = Math.abs(point.cost - other.cost)
const scoreDiff = Math.abs(point.score - other.score)
// Check if label would be placed on top of another point.
switch (position) {
case "top":
// Label is above, check if there's a point above.
if (costDiff < 3 && other.score > point.score && other.score - point.score < 6) {
return true
}
break
case "bottom":
// Label is below, check if there's a point below.
if (costDiff < 3 && other.score < point.score && point.score - other.score < 6) {
return true
}
break
case "left":
// Label is to the left, check if there's a point to the left.
if (scoreDiff < 3 && other.cost < point.cost && point.cost - other.cost < 4) {
return true
}
break
case "right":
// Label is to the right, check if there's a point to the right.
if (scoreDiff < 3 && other.cost > point.cost && other.cost - point.cost < 4) {
return true
}
break
}
}
return false
}
// Sort points to process them in a consistent order.
// Process from top-left to bottom-right.
const sortedData = [...chartData].sort((a, b) => {
// First by score (higher first).
const scoreDiff = b.score - a.score
if (Math.abs(scoreDiff) > 1) return scoreDiff
// Then by cost (lower first).
return a.cost - b.cost
})
// Process each point and find the best position.
sortedData.forEach((point) => {
// Try positions in order of preference.
const positionPreferences: LabelPosition[] = ["top", "bottom", "right", "left"]
let bestPosition: LabelPosition = "top"
for (const position of positionPreferences) {
// Check if this position would overlap with any placed labels.
let hasLabelOverlap = false
for (const placed of placedLabels) {
if (
wouldLabelsOverlap(
{ cost: point.cost, score: point.score, position },
{ cost: placed.cost, score: placed.score, position: placed.position },
)
) {
hasLabelOverlap = true
break
}
}
// Check if this position would overlap with any data points.
const hasPointOverlap = wouldOverlapPoint(point, position)
// If no overlaps, use this position.
if (!hasLabelOverlap && !hasPointOverlap) {
bestPosition = position
break
}
}
// Use the best position found
positions[point.label] = bestPosition
placedLabels.push({
cost: point.cost,
score: point.score,
label: point.label,
position: bestPosition,
})
})
return positions
}, [chartData])
return (
<>
<div className="pt-4 pb-8 font-mono">Cost x Score</div>
<ChartContainer config={chartConfig} className="h-[500px] w-full">
<ScatterChart margin={{ top: 20, right: 0, bottom: 0, left: 20 }}>
<XAxis
type="number"
dataKey="cost"
name="Cost"
domain={[
(dataMin: number) => Math.max(0, Math.round((dataMin - 5) / 5) * 5),
(dataMax: number) => Math.round((dataMax + 5) / 5) * 5,
]}
tickFormatter={(value) => formatCurrency(value)}
/>
<YAxis
type="number"
dataKey="score"
name="Score"
domain={[
(dataMin: number) => Math.max(0, Math.round((dataMin - 5) / 5) * 5),
(dataMax: number) => Math.min(100, Math.round((dataMax + 5) / 5) * 5),
]}
tickFormatter={(value) => `${value}%`}
/>
<ChartTooltip
content={({ active, payload }) => {
if (!active || !payload || !payload.length || !payload[0]) {
return null
}
const { label, cost, score } = payload[0].payload
return (
<div className="bg-background border rounded-sm p-2 shadow-sm text-left">
<div className="border-b pb-1">{label}</div>
<div className="pt-1">
<div>
Score: <span className="font-mono">{Math.round(score)}%</span>
</div>
<div>
Cost: <span className="font-mono">{formatCurrency(cost)}</span>
</div>
</div>
</div>
)
}}
/>
<Customized component={renderQuadrant} />
{chartData.map((d, index) => (
<Scatter
key={d.label}
name={d.label}
data={[d]}
fill={generateSpectrumColor(index, chartData.length)}>
<LabelList
dataKey="label"
content={(props) => renderCustomLabel(props, labelPositions[d.label] || "top")}
/>
</Scatter>
))}
</ScatterChart>
</ChartContainer>
<div className="py-4 text-xs opacity-50">
(Note: Models with a cost of $50 or more are excluded from the scatter plot.)
</div>
</>
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderQuadrant = (props: any) => (
<Cross
width={props.width}
height={props.height}
x={props.width / 2 + 35}
y={props.height / 2 - 15}
top={0}
left={0}
stroke="currentColor"
opacity={0.1}
/>
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderCustomLabel = (props: any, position: LabelPosition) => {
const { x, y, value } = props
const maxWidth = 80 // Maximum width in pixels - adjust as needed.
const truncateText = (text: string, maxChars: number = 20) => {
if (text.length <= maxChars) {
return text
}
return text.substring(0, maxChars - 1) + "…"
}
// Calculate position offsets based on label position.
let xOffset = 0
let yOffset = 0
let textAnchor: "middle" | "start" | "end" = "middle"
let dominantBaseline: "auto" | "hanging" | "middle" = "auto"
switch (position) {
case "top":
yOffset = -8
textAnchor = "middle"
dominantBaseline = "auto"
break
case "bottom":
yOffset = 15
textAnchor = "middle"
dominantBaseline = "hanging"
break
case "left":
xOffset = -8
yOffset = 5
textAnchor = "end"
dominantBaseline = "middle"
break
case "right":
xOffset = 15
yOffset = 5
textAnchor = "start"
dominantBaseline = "middle"
break
}
return (
<text
x={x + xOffset}
y={y + yOffset}
fontSize="11"
fontWeight="500"
fill="currentColor"
opacity="0.8"
textAnchor={textAnchor}
dominantBaseline={dominantBaseline}
style={{
pointerEvents: "none",
maxWidth: `${maxWidth}px`,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{truncateText(value)}
</text>
)
}
const generateSpectrumColor = (index: number, total: number): string => {
// Distribute hues evenly across the color wheel (0-360 degrees).
// Start at 0 (red) and distribute evenly.
const hue = (index * 360) / total
// Use high saturation for vibrant colors.
const saturation = 70
// Use medium lightness for good visibility on both light and dark backgrounds.
const lightness = 50
return `hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`
}

View file

@ -1,9 +0,0 @@
import type { TaskMetrics, Run } from "@roo-code/evals"
export type EvalRun = Run & {
label: string
score: number
languageScores?: Record<"go" | "java" | "javascript" | "python" | "rust", number>
taskMetrics: TaskMetrics
modelId?: string
}

View file

@ -69,13 +69,19 @@ export function NavBar({ stars, downloads }: NavBarProps) {
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Community
</a>
<a
href={EXTERNAL_LINKS.CLOUD_APP}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Cloud
</a>
<div className="flex items-center rounded-full bg-gradient-to-r from-blue-400 to-cyan-400 p-0.5 text-xs">
<div className="rounded-full bg-background px-2 py-1.5">
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-1.5">
Roo Code Cloud is coming
</span>
<a
href="/cloud-waitlist"
rel="noopener noreferrer"
className="font-medium text-primary hover:underline pl-1.5">
Sign up
</a>
</div>
</div>
</nav>
<div className="hidden md:flex md:items-center md:space-x-4">
@ -115,6 +121,19 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<div
className={`absolute left-0 right-0 top-16 z-50 transform border-b border-border bg-background shadow-lg backdrop-blur-none transition-all duration-200 md:hidden ${isMenuOpen ? "translate-y-0 opacity-100" : "pointer-events-none -translate-y-2 opacity-0"}`}>
<nav className="flex flex-col py-2">
<div className="mx-5 mb-2 flex items-center rounded-full bg-gradient-to-r from-blue-400 to-cyan-400 p-0.5 text-xs">
<div className="flex-grow text-center rounded-full bg-background px-2 py-1.5">
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-3">
Roo Code Cloud is coming
</span>
<a
href="/cloud-waitlist"
rel="noopener noreferrer"
className="font-medium text-primary hover:underline pl-3">
Sign up
</a>
</div>
</div>
<ScrollButton
targetId="features"
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
@ -162,14 +181,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
onClick={() => setIsMenuOpen(false)}>
Community
</a>
<a
href={EXTERNAL_LINKS.CLOUD_APP}
target="_blank"
rel="noopener noreferrer"
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setIsMenuOpen(false)}>
Cloud
</a>
<hr className="mx-8 my-2 border-t border-border/50" />

View file

@ -24,7 +24,6 @@ export const EXTERNAL_LINKS = {
OFFICE_HOURS_PODCAST: "https://www.youtube.com/@RooCodeYT/podcasts",
FAQ: "https://roocode.com/#faq",
TESTIMONIALS: "https://roocode.com/#testimonials",
CLOUD_APP: "https://app.roocode.com",
}
export const INTERNAL_LINKS = {

View file

@ -3,12 +3,6 @@ const formatter = new Intl.NumberFormat("en-US", {
currency: "USD",
})
export const formatCurrency = (amount: number | null | undefined) => {
if (amount === null || amount === undefined) {
return "-"
}
return formatter.format(amount)
}
export const formatCurrency = (amount: number) => formatter.format(amount)
export const parsePrice = (price?: string) => (price ? parseFloat(price) * 1_000_000 : undefined)

View file

@ -1,8 +1,4 @@
export const formatDuration = (durationMs: number | null | undefined) => {
if (durationMs === null || durationMs === undefined) {
return "-"
}
export const formatDuration = (durationMs: number) => {
const seconds = Math.floor(durationMs / 1000)
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)

View file

@ -1,8 +1,4 @@
export const formatTokens = (tokens: number | null | undefined, decimals = 0) => {
if (tokens === null || tokens === undefined) {
return "-"
}
export const formatTokens = (tokens: number, decimals = 0) => {
if (tokens < 1000) {
return tokens.toString()
}

View file

@ -49,7 +49,7 @@ export const getOpenRouterModels = async (): Promise<OpenRouterModelRecord> => {
return result.data.data
.filter((rawModel) => {
// Skip image generation models (models that output images).
// Skip image generation models (models that output images)
return !rawModel.architecture?.output_modalities?.includes("image")
})
.sort((a, b) => a.name.localeCompare(b.name))

View file

@ -248,11 +248,6 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this.settingsService!.updateUserSettings(settings)
}
public isTaskSyncEnabled(): boolean {
this.ensureInitialized()
return this.settingsService!.isTaskSyncEnabled()
}
// TelemetryClient
public captureEvent(event: TelemetryEvent): void {

View file

@ -266,21 +266,6 @@ export class CloudSettingsService extends EventEmitter<SettingsServiceEvents> im
}
}
public isTaskSyncEnabled(): boolean {
// Org settings take precedence
if (this.authService.getStoredOrganizationId()) {
return this.settings?.cloudSettings?.recordTaskMessages ?? false
}
// User settings default to true if unspecified
const userSettings = this.userSettings
if (userSettings) {
return userSettings.settings.taskSyncEnabled ?? true
}
return false
}
private async removeSettings(): Promise<void> {
this.settings = undefined
this.userSettings = undefined

View file

@ -51,7 +51,6 @@ export class StaticSettingsService implements SettingsService {
},
settings: {
extensionBridgeEnabled: true,
taskSyncEnabled: true,
},
version: 1,
}
@ -66,7 +65,6 @@ export class StaticSettingsService implements SettingsService {
public getUserSettingsConfig(): UserSettingsConfig {
return {
extensionBridgeEnabled: true,
taskSyncEnabled: true,
}
}
@ -74,11 +72,6 @@ export class StaticSettingsService implements SettingsService {
throw new Error("User settings updates are not supported in static mode")
}
public isTaskSyncEnabled(): boolean {
// Static settings always enable task sync
return true
}
public dispose(): void {
// No resources to clean up for static settings.
}

View file

@ -233,9 +233,9 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
return false
}
// Only record message telemetry if task sync is enabled
// Only record message telemetry if a cloud account is present and explicitly configured to record messages
if (eventName === TelemetryEventName.TASK_MESSAGE) {
return this.settingsService.isTaskSyncEnabled()
return this.settingsService.getSettings()?.cloudSettings?.recordTaskMessages || false
}
// Other telemetry types are capturable at this point

View file

@ -59,7 +59,6 @@ describe("CloudService", () => {
initialize: ReturnType<typeof vi.fn>
getSettings: ReturnType<typeof vi.fn>
getAllowList: ReturnType<typeof vi.fn>
isTaskSyncEnabled: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
off: ReturnType<typeof vi.fn>
@ -131,7 +130,6 @@ describe("CloudService", () => {
initialize: vi.fn(),
getSettings: vi.fn(),
getAllowList: vi.fn(),
isTaskSyncEnabled: vi.fn().mockReturnValue(true),
dispose: vi.fn(),
on: vi.fn(),
off: vi.fn(),
@ -345,12 +343,6 @@ describe("CloudService", () => {
cloudService.getAllowList()
expect(mockSettingsService.getAllowList).toHaveBeenCalled()
})
it("should delegate isTaskSyncEnabled to SettingsService", () => {
const result = cloudService.isTaskSyncEnabled()
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
expect(result).toBe(true)
})
})
describe("error handling", () => {

View file

@ -20,7 +20,6 @@ describe("CloudSettingsService", () => {
getSessionToken: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
getStoredOrganizationId: ReturnType<typeof vi.fn>
}
let mockRefreshTimer: {
start: ReturnType<typeof vi.fn>
@ -64,7 +63,6 @@ describe("CloudSettingsService", () => {
getSessionToken: vi.fn(),
hasActiveSession: vi.fn().mockReturnValue(false),
on: vi.fn(),
getStoredOrganizationId: vi.fn().mockReturnValue(null),
}
mockRefreshTimer = {
@ -534,191 +532,4 @@ describe("CloudSettingsService", () => {
expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", undefined)
})
})
describe("isTaskSyncEnabled", () => {
beforeEach(async () => {
await cloudSettingsService.initialize()
})
it("should return true when org recordTaskMessages is true", () => {
// Set up mock settings with org recordTaskMessages = true
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: true,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when org recordTaskMessages is false", () => {
// Set up mock settings with org recordTaskMessages = false
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: false,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should fall back to user taskSyncEnabled when org recordTaskMessages is undefined", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: true,
},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when user taskSyncEnabled is false", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: false,
},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should return true when user taskSyncEnabled is undefined (default)", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when no settings are available", () => {
// Mock that user has no organization ID
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Clear both settings
;(cloudSettingsService as unknown as { settings: undefined }).settings = undefined
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should return false when only org settings are available but cloudSettings is undefined", () => {
const mockSettings = {
version: 1,
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should prioritize org settings over user settings", () => {
// Set up conflicting settings: org = false, user = true
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: false,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: true,
},
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
// Should return false (org setting takes precedence)
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
})
})

View file

@ -98,28 +98,5 @@ describe("StaticSettingsService", () => {
expect(mockLog).not.toHaveBeenCalled()
})
describe("isTaskSyncEnabled", () => {
it("should always return true", () => {
const service = new StaticSettingsService(validBase64)
expect(service.isTaskSyncEnabled()).toBe(true)
})
it("should return true regardless of settings content", () => {
// Create settings with different content
const differentSettings = {
version: 2,
cloudSettings: {
recordTaskMessages: false,
},
defaultSettings: {},
allowList: { allowAll: false, providers: {} },
}
const differentBase64 = Buffer.from(JSON.stringify(differentSettings)).toString("base64")
const service = new StaticSettingsService(differentBase64)
expect(service.isTaskSyncEnabled()).toBe(true)
})
})
})
})

View file

@ -35,14 +35,6 @@ describe("TelemetryClient", () => {
recordTaskMessages: true,
},
}),
getUserSettings: vi.fn().mockReturnValue({
features: {},
settings: {
taskSyncEnabled: true,
},
version: 1,
}),
isTaskSyncEnabled: vi.fn().mockReturnValue(true),
}
mockFetch.mockResolvedValue({
@ -84,8 +76,12 @@ describe("TelemetryClient", () => {
expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
})
it("should return true for TASK_MESSAGE events when isTaskSyncEnabled returns true", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {
recordTaskMessages: true,
},
})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
@ -95,11 +91,55 @@ describe("TelemetryClient", () => {
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should return false for TASK_MESSAGE events when isTaskSyncEnabled returns false", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
it("should return false for TASK_MESSAGE events when recordTaskMessages is false", () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {
recordTaskMessages: false,
},
})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
})
it("should return false for TASK_MESSAGE events when recordTaskMessages is undefined", () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {},
})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
})
it("should return false for TASK_MESSAGE events when cloudSettings is undefined", () => {
mockSettingsService.getSettings.mockReturnValue({})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
})
it("should return false for TASK_MESSAGE events when getSettings returns undefined", () => {
mockSettingsService.getSettings.mockReturnValue(undefined)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
@ -109,7 +149,6 @@ describe("TelemetryClient", () => {
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
})
@ -234,8 +273,10 @@ describe("TelemetryClient", () => {
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not capture TASK_MESSAGE events when isTaskSyncEnabled returns false", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {},
})
const client = new TelemetryClient(mockAuthService, mockSettingsService)
@ -253,7 +294,6 @@ describe("TelemetryClient", () => {
})
expect(mockFetch).not.toHaveBeenCalled()
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should not send request when schema validation fails", async () => {
@ -313,8 +353,12 @@ describe("TelemetryClient", () => {
)
})
it("should attempt to capture TASK_MESSAGE events when isTaskSyncEnabled returns true", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => {
mockSettingsService.getSettings.mockReturnValue({
cloudSettings: {
recordTaskMessages: true,
},
})
const eventProperties = {
appName: "roo-code",
@ -345,7 +389,6 @@ describe("TelemetryClient", () => {
properties: eventProperties,
})
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({

View file

@ -15,8 +15,6 @@
"drizzle-kit:production": "dotenvx run -f .env.production -- tsx node_modules/drizzle-kit/bin.cjs",
"db:generate": "pnpm drizzle-kit generate",
"db:migrate": "pnpm drizzle-kit migrate",
"db:test:migrate": "pnpm drizzle-kit:test migrate",
"db:production:migrate": "pnpm drizzle-kit:production migrate",
"db:push": "pnpm drizzle-kit push",
"db:test:push": "pnpm drizzle-kit:test push",
"db:production:push": "pnpm drizzle-kit:production push",

View file

@ -1,6 +0,0 @@
ALTER TABLE "runs" ADD COLUMN "name" text;--> statement-breakpoint
ALTER TABLE "runs" ADD COLUMN "contextWindow" integer;--> statement-breakpoint
ALTER TABLE "runs" ADD COLUMN "inputPrice" real;--> statement-breakpoint
ALTER TABLE "runs" ADD COLUMN "outputPrice" real;--> statement-breakpoint
ALTER TABLE "runs" ADD COLUMN "cacheWritesPrice" real;--> statement-breakpoint
ALTER TABLE "runs" ADD COLUMN "cacheReadsPrice" real;

View file

@ -1,453 +0,0 @@
{
"id": "3d2b8423-6170-4cb2-9f62-1c86756da97a",
"prevId": "43b197c4-ff4f-48c1-908b-a330e66a162d",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.runs": {
"name": "runs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "runs_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"task_metrics_id": {
"name": "task_metrics_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"contextWindow": {
"name": "contextWindow",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"inputPrice": {
"name": "inputPrice",
"type": "real",
"primaryKey": false,
"notNull": false
},
"outputPrice": {
"name": "outputPrice",
"type": "real",
"primaryKey": false,
"notNull": false
},
"cacheWritesPrice": {
"name": "cacheWritesPrice",
"type": "real",
"primaryKey": false,
"notNull": false
},
"cacheReadsPrice": {
"name": "cacheReadsPrice",
"type": "real",
"primaryKey": false,
"notNull": false
},
"settings": {
"name": "settings",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"pid": {
"name": "pid",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"socket_path": {
"name": "socket_path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"concurrency": {
"name": "concurrency",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 2
},
"timeout": {
"name": "timeout",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 5
},
"passed": {
"name": "passed",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"failed": {
"name": "failed",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"runs_task_metrics_id_taskMetrics_id_fk": {
"name": "runs_task_metrics_id_taskMetrics_id_fk",
"tableFrom": "runs",
"tableTo": "taskMetrics",
"columnsFrom": ["task_metrics_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.taskMetrics": {
"name": "taskMetrics",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "taskMetrics_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"tokens_in": {
"name": "tokens_in",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"tokens_out": {
"name": "tokens_out",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"tokens_context": {
"name": "tokens_context",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"cache_writes": {
"name": "cache_writes",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"cache_reads": {
"name": "cache_reads",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"cost": {
"name": "cost",
"type": "real",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"tool_usage": {
"name": "tool_usage",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.tasks": {
"name": "tasks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "tasks_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"run_id": {
"name": "run_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"task_metrics_id": {
"name": "task_metrics_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true
},
"exercise": {
"name": "exercise",
"type": "text",
"primaryKey": false,
"notNull": true
},
"passed": {
"name": "passed",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"finished_at": {
"name": "finished_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"tasks_language_exercise_idx": {
"name": "tasks_language_exercise_idx",
"columns": [
{
"expression": "run_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "language",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "exercise",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"tasks_run_id_runs_id_fk": {
"name": "tasks_run_id_runs_id_fk",
"tableFrom": "tasks",
"tableTo": "runs",
"columnsFrom": ["run_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"tasks_task_metrics_id_taskMetrics_id_fk": {
"name": "tasks_task_metrics_id_taskMetrics_id_fk",
"tableFrom": "tasks",
"tableTo": "taskMetrics",
"columnsFrom": ["task_metrics_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.toolErrors": {
"name": "toolErrors",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "toolErrors_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"run_id": {
"name": "run_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"task_id": {
"name": "task_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"tool_name": {
"name": "tool_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"toolErrors_run_id_runs_id_fk": {
"name": "toolErrors_run_id_runs_id_fk",
"tableFrom": "toolErrors",
"tableTo": "runs",
"columnsFrom": ["run_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"toolErrors_task_id_tasks_id_fk": {
"name": "toolErrors_task_id_tasks_id_fk",
"tableFrom": "toolErrors",
"tableTo": "tasks",
"columnsFrom": ["task_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -15,13 +15,6 @@
"when": 1753198630651,
"tag": "0001_lowly_captain_flint",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1757191027855,
"tag": "0002_bouncy_blazing_skull",
"breakpoints": true
}
]
}

View file

@ -13,13 +13,7 @@ export const runs = pgTable("runs", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
taskMetricsId: integer("task_metrics_id").references(() => taskMetrics.id),
model: text().notNull(),
name: text(),
description: text(),
contextWindow: integer(),
inputPrice: real(),
outputPrice: real(),
cacheWritesPrice: real(),
cacheReadsPrice: real(),
settings: jsonb().$type<RooCodeSettings>(),
pid: integer(),
socketPath: text("socket_path").notNull(),

View file

@ -36,14 +36,14 @@ export class TelemetryService {
/**
* Updates the telemetry state based on user preferences and VSCode settings
* @param isOptedIn Whether the user is opted into telemetry
* @param didUserOptIn Whether the user has explicitly opted into telemetry
*/
public updateTelemetryState(isOptedIn: boolean): void {
public updateTelemetryState(didUserOptIn: boolean): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.updateTelemetryState(isOptedIn))
this.clients.forEach((client) => client.updateTelemetryState(didUserOptIn))
}
/**

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.75.0",
"version": "1.74.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",

View file

@ -162,7 +162,6 @@ export type UserFeatures = z.infer<typeof userFeaturesSchema>
export const userSettingsConfigSchema = z.object({
extensionBridgeEnabled: z.boolean().optional(),
taskSyncEnabled: z.boolean().optional(),
})
export type UserSettingsConfig = z.infer<typeof userSettingsConfigSchema>
@ -303,14 +302,6 @@ export interface SettingsService {
*/
updateUserSettings(settings: Partial<UserSettingsConfig>): Promise<boolean>
/**
* Determines if task sync/recording is enabled based on organization and user settings
* Organization settings take precedence over user settings.
* User settings default to true if unspecified.
* @returns true if task sync is enabled, false otherwise
*/
isTaskSyncEnabled(): boolean
/**
* Dispose of the settings service and clean up resources
*/

View file

@ -41,7 +41,6 @@ export const globalSettingsSchema = z.object({
lastShownAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
taskHistory: z.array(historyItemSchema).optional(),
dismissedUpsells: z.array(z.string()).optional(),
// Image generation settings (experimental) - flattened for simplicity
openRouterImageApiKey: z.string().optional(),
@ -139,6 +138,8 @@ export const globalSettingsSchema = z.object({
mcpEnabled: z.boolean().optional(),
enableMcpServerCreation: z.boolean().optional(),
remoteControlEnabled: z.boolean().optional(),
mode: z.string().optional(),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
customModes: z.array(modeConfigSchema).optional(),
@ -315,6 +316,8 @@ export const EVALS_SETTINGS: RooCodeSettings = {
mcpEnabled: false,
remoteControlEnabled: false,
mode: "code", // "architect",
customModes: [],

View file

@ -294,60 +294,6 @@ export const vertexModels = {
outputPrice: 1.15,
description: "Meta Llama 4 Maverick 17B Instruct model, 128K context.",
},
"deepseek-r1-0528-maas": {
maxTokens: 32_768,
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.35,
outputPrice: 5.4,
description: "DeepSeek R1 (0528). Available in us-central1",
},
"deepseek-v3.1-maas": {
maxTokens: 32_768,
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.6,
outputPrice: 1.7,
description: "DeepSeek V3.1. Available in us-west2",
},
"gpt-oss-120b-maas": {
maxTokens: 32_768,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.6,
description: "OpenAI gpt-oss 120B. Available in us-central1",
},
"gpt-oss-20b-maas": {
maxTokens: 32_768,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.075,
outputPrice: 0.3,
description: "OpenAI gpt-oss 20B. Available in us-central1",
},
"qwen3-coder-480b-a35b-instruct-maas": {
maxTokens: 32_768,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.0,
outputPrice: 4.0,
description: "Qwen3 Coder 480B A35B Instruct. Available in us-south1",
},
"qwen3-235b-a22b-instruct-2507-maas": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.25,
outputPrice: 1.0,
description: "Qwen3 235B A22B Instruct. Available in us-south1",
},
} as const satisfies Record<string, ModelInfo>
export const VERTEX_REGIONS = [
@ -356,7 +302,6 @@ export const VERTEX_REGIONS = [
{ value: "us-east1", label: "us-east1" },
{ value: "us-east4", label: "us-east4" },
{ value: "us-east5", label: "us-east5" },
{ value: "us-south1", label: "us-south1" },
{ value: "us-west1", label: "us-west1" },
{ value: "us-west2", label: "us-west2" },
{ value: "us-west3", label: "us-west3" },

View file

@ -242,7 +242,7 @@ export interface TelemetryClient {
setProvider(provider: TelemetryPropertiesProvider): void
capture(options: TelemetryEvent): Promise<void>
updateTelemetryState(isOptedIn: boolean): void
updateTelemetryState(didUserOptIn: boolean): void
isTelemetryEnabled(): boolean
shutdown(): Promise<void>
}

628
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -409,10 +409,6 @@ describe("ChutesHandler", () => {
content: `${systemPrompt}\n${messages[0].content}`,
},
],
max_tokens: 32768,
temperature: 0.6,
stream: true,
stream_options: { include_usage: true },
}),
)
})
@ -442,7 +438,6 @@ describe("ChutesHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },

View file

@ -373,7 +373,6 @@ describe("FireworksHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },

View file

@ -149,7 +149,7 @@ describe("GroqHandler", () => {
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toMatchObject({
type: "usage",
inputTokens: 100,
inputTokens: 70, // 100 total - 30 cached
outputTokens: 50,
cacheWriteTokens: 0,
cacheReadTokens: 30,
@ -160,7 +160,11 @@ describe("GroqHandler", () => {
it("createMessage should pass correct parameters to Groq client", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const modelInfo = groqModels[modelId]
const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" })
const handlerWithModel = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
modelTemperature: 0.5, // Explicitly set temperature for this test
})
mockCreate.mockImplementationOnce(() => {
return {
@ -190,4 +194,77 @@ describe("GroqHandler", () => {
undefined,
)
})
it("should omit temperature when modelTemperature is undefined", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const handlerWithoutTemp = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
// modelTemperature is not set
})
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 = handlerWithoutTemp.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
}),
undefined,
)
// Verify temperature is NOT included
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("temperature")
})
it("should include temperature when modelTemperature is explicitly set", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const handlerWithTemp = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
modelTemperature: 0.7,
})
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 = handlerWithTemp.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
temperature: 0.7,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
}),
undefined,
)
})
})

View file

@ -315,6 +315,71 @@ describe("OpenAiHandler", () => {
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.max_completion_tokens).toBe(4096)
})
it("should omit temperature when modelTemperature is undefined", async () => {
const optionsWithoutTemperature: ApiHandlerOptions = {
...mockOptions,
// modelTemperature is not set, should not include temperature
}
const handlerWithoutTemperature = new OpenAiHandler(optionsWithoutTemperature)
const stream = handlerWithoutTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called without temperature
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("temperature")
})
it("should include temperature when modelTemperature is explicitly set to 0", async () => {
const optionsWithZeroTemperature: ApiHandlerOptions = {
...mockOptions,
modelTemperature: 0,
}
const handlerWithZeroTemperature = new OpenAiHandler(optionsWithZeroTemperature)
const stream = handlerWithZeroTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with temperature: 0
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0)
})
it("should include temperature when modelTemperature is set to a non-zero value", async () => {
const optionsWithCustomTemperature: ApiHandlerOptions = {
...mockOptions,
modelTemperature: 0.7,
}
const handlerWithCustomTemperature = new OpenAiHandler(optionsWithCustomTemperature)
const stream = handlerWithCustomTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with temperature: 0.7
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0.7)
})
it("should include DEEP_SEEK_DEFAULT_TEMPERATURE for deepseek-reasoner models when temperature is not set", async () => {
const deepseekOptions: ApiHandlerOptions = {
...mockOptions,
openAiModelId: "deepseek-reasoner",
// modelTemperature is not set
}
const deepseekHandler = new OpenAiHandler(deepseekOptions)
const stream = deepseekHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with DEEP_SEEK_DEFAULT_TEMPERATURE (0.6)
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0.6)
})
})
describe("error handling", () => {
@ -450,7 +515,7 @@ describe("OpenAiHandler", () => {
],
stream: true,
stream_options: { include_usage: true },
temperature: 0,
// temperature should be omitted when not set
},
{ path: "/models/chat/completions" },
)

View file

@ -354,7 +354,7 @@ describe("RooHandler", () => {
})
describe("temperature and model configuration", () => {
it("should use default temperature of 0.7", async () => {
it("should omit temperature when not explicitly set", async () => {
handler = new RooHandler(mockOptions)
const stream = handler.createMessage(systemPrompt, messages)
for await (const _chunk of stream) {
@ -362,8 +362,8 @@ describe("RooHandler", () => {
}
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
temperature: 0.7,
expect.not.objectContaining({
temperature: expect.anything(),
}),
undefined,
)

View file

@ -144,7 +144,6 @@ describe("SambaNovaHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.7,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },

View file

@ -220,7 +220,6 @@ describe("ZAiHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: ZAI_DEFAULT_TEMPERATURE,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },

View file

@ -74,17 +74,19 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
info: { maxTokens: max_tokens },
} = this.getModel()
const temperature = this.options.modelTemperature ?? this.defaultTemperature
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model,
max_tokens,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
}
// Only include temperature if explicitly set
if (this.options.modelTemperature !== undefined) {
params.temperature = this.options.modelTemperature
}
try {
return this.client.chat.completions.create(params, requestOptions)
} catch (error) {

View file

@ -66,9 +66,20 @@ export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
// Calculate cost using OpenAI-compatible cost calculation
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
// Calculate non-cached input tokens for proper reporting
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
console.log("usage", {
inputTokens: nonCachedInputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
yield {
type: "usage",
inputTokens,
inputTokens: nonCachedInputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,

View file

@ -159,13 +159,20 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelId,
temperature: this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
messages: convertedMessages,
stream: true as const,
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
...(reasoning && reasoning),
}
// Only include temperature if explicitly set
if (this.options.modelTemperature !== undefined) {
requestOptions.temperature = this.options.modelTemperature
} else if (deepseekReasoner) {
// DeepSeek Reasoner has a specific default temperature
requestOptions.temperature = DEEP_SEEK_DEFAULT_TEMPERATURE
}
// Add max_tokens if needed
this.addMaxTokensIfNeeded(requestOptions, modelInfo)

View file

@ -1,260 +0,0 @@
// npx vitest src/core/condense/__tests__/condense.spec.ts
import { Anthropic } from "@anthropic-ai/sdk"
import type { ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { BaseProvider } from "../../../api/providers/base-provider"
import { ApiMessage } from "../../task-persistence/apiMessages"
import { summarizeConversation, getMessagesSinceLastSummary, N_MESSAGES_TO_KEEP } from "../index"
// Create a mock ApiHandler for testing
class MockApiHandler extends BaseProvider {
createMessage(): any {
// Mock implementation for testing - returns an async iterable stream
const mockStream = {
async *[Symbol.asyncIterator]() {
yield { type: "text", text: "Mock summary of the conversation" }
yield { type: "usage", inputTokens: 100, outputTokens: 50, totalCost: 0.01 }
},
}
return mockStream
}
getModel(): { id: string; info: ModelInfo } {
return {
id: "test-model",
info: {
contextWindow: 100000,
maxTokens: 50000,
supportsPromptCache: true,
supportsImages: false,
inputPrice: 0,
outputPrice: 0,
description: "Test model",
},
}
}
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
// Simple token counting for testing
let tokens = 0
for (const block of content) {
if (block.type === "text") {
tokens += Math.ceil(block.text.length / 4) // Rough approximation
}
}
return tokens
}
}
const mockApiHandler = new MockApiHandler()
const taskId = "test-task-id"
describe("Condense", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})
describe("summarizeConversation", () => {
it("should preserve the first message when summarizing", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message with /prr command content" },
{ role: "assistant", content: "Second message" },
{ role: "user", content: "Third message" },
{ role: "assistant", content: "Fourth message" },
{ role: "user", content: "Fifth message" },
{ role: "assistant", content: "Sixth message" },
{ role: "user", content: "Seventh message" },
{ role: "assistant", content: "Eighth message" },
{ role: "user", content: "Ninth message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
// Verify the first message is preserved
expect(result.messages[0]).toEqual(messages[0])
expect(result.messages[0].content).toBe("First message with /prr command content")
// Verify we have a summary message
const summaryMessage = result.messages.find((msg) => msg.isSummary)
expect(summaryMessage).toBeTruthy()
expect(summaryMessage?.content).toBe("Mock summary of the conversation")
// Verify we have the expected number of messages
// [first message, summary, last N messages]
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP)
// Verify the last N messages are preserved
const lastMessages = result.messages.slice(-N_MESSAGES_TO_KEEP)
expect(lastMessages).toEqual(messages.slice(-N_MESSAGES_TO_KEEP))
})
it("should preserve slash command content in the first message", async () => {
const slashCommandContent = "/prr #123 - Fix authentication bug"
const messages: ApiMessage[] = [
{ role: "user", content: slashCommandContent },
{ role: "assistant", content: "I'll help you fix that authentication bug" },
{ role: "user", content: "The issue is with JWT tokens" },
{ role: "assistant", content: "Let me examine the JWT implementation" },
{ role: "user", content: "It's failing on refresh" },
{ role: "assistant", content: "I found the issue" },
{ role: "user", content: "Great, can you fix it?" },
{ role: "assistant", content: "Here's the fix" },
{ role: "user", content: "Thanks!" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
// The first message with slash command should be intact
expect(result.messages[0].content).toBe(slashCommandContent)
expect(result.messages[0]).toEqual(messages[0])
})
it("should handle complex first message content", async () => {
const complexContent: Anthropic.Messages.ContentBlockParam[] = [
{ type: "text", text: "/mode code" },
{ type: "text", text: "Additional context from the user" },
]
const messages: ApiMessage[] = [
{ role: "user", content: complexContent },
{ role: "assistant", content: "Switching to code mode" },
{ role: "user", content: "Write a function" },
{ role: "assistant", content: "Here's the function" },
{ role: "user", content: "Add error handling" },
{ role: "assistant", content: "Added error handling" },
{ role: "user", content: "Add tests" },
{ role: "assistant", content: "Tests added" },
{ role: "user", content: "Perfect!" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
// The first message with complex content should be preserved
expect(result.messages[0].content).toEqual(complexContent)
expect(result.messages[0]).toEqual(messages[0])
})
it("should return error when not enough messages to summarize", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message with /command" },
{ role: "assistant", content: "Second message" },
{ role: "user", content: "Third message" },
{ role: "assistant", content: "Fourth message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
// Should return an error since we have only 4 messages (first + 3 to keep)
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages) // Original messages unchanged
expect(result.summary).toBe("")
})
it("should not summarize messages that already contain a recent summary", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message with /command" },
{ role: "assistant", content: "Old message" },
{ role: "user", content: "Message before summary" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Another message" },
{ role: "assistant", content: "Previous summary", isSummary: true }, // Summary in last N messages
{ role: "user", content: "Final message" },
]
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
// Should return an error due to recent summary in last N messages
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages)
expect(result.summary).toBe("")
})
it("should handle empty summary from API gracefully", async () => {
// Mock handler that returns empty summary
class EmptyMockApiHandler extends MockApiHandler {
override createMessage(): any {
const mockStream = {
async *[Symbol.asyncIterator]() {
yield { type: "text", text: "" }
yield { type: "usage", inputTokens: 100, outputTokens: 0, totalCost: 0.01 }
},
}
return mockStream
}
}
const emptyHandler = new EmptyMockApiHandler()
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Second" },
{ role: "user", content: "Third" },
{ role: "assistant", content: "Fourth" },
{ role: "user", content: "Fifth" },
{ role: "assistant", content: "Sixth" },
{ role: "user", content: "Seventh" },
]
const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false)
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages)
expect(result.cost).toBeGreaterThan(0)
})
})
describe("getMessagesSinceLastSummary", () => {
it("should return all messages when no summary exists", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Second message" },
{ role: "user", content: "Third message" },
]
const result = getMessagesSinceLastSummary(messages)
expect(result).toEqual(messages)
})
it("should return messages since last summary including the summary", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Second message" },
{ role: "assistant", content: "Summary content", isSummary: true },
{ role: "user", content: "Message after summary" },
{ role: "assistant", content: "Final message" },
]
const result = getMessagesSinceLastSummary(messages)
// Should include a user message prefix for Bedrock compatibility, the summary, and messages after
expect(result[0].role).toBe("user")
expect(result[0].content).toBe("Please continue from the following summary:")
expect(result[1]).toEqual(messages[2]) // The summary
expect(result[2]).toEqual(messages[3])
expect(result[3]).toEqual(messages[4])
})
it("should handle multiple summaries and return from the last one", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "First summary", isSummary: true },
{ role: "user", content: "Middle message" },
{ role: "assistant", content: "Second summary", isSummary: true },
{ role: "user", content: "Recent message" },
{ role: "assistant", content: "Final message" },
]
const result = getMessagesSinceLastSummary(messages)
// Should only include from the last summary
expect(result[0].role).toBe("user")
expect(result[0].content).toBe("Please continue from the following summary:")
expect(result[1]).toEqual(messages[3]) // Second summary
expect(result[2]).toEqual(messages[4])
expect(result[3]).toEqual(messages[5])
})
})
})

View file

@ -188,14 +188,11 @@ describe("summarizeConversation", () => {
expect(maybeRemoveImageBlocks).toHaveBeenCalled()
// Verify the structure of the result
// The result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
// Check that the first message is preserved
expect(result.messages[0]).toEqual(messages[0])
// The result should be: original messages (except last N) + summary + last N messages
expect(result.messages.length).toBe(messages.length + 1) // Original + summary
// Check that the summary message was inserted correctly
const summaryMessage = result.messages[1]
const summaryMessage = result.messages[result.messages.length - N_MESSAGES_TO_KEEP - 1]
expect(summaryMessage.role).toBe("assistant")
expect(summaryMessage.content).toBe("This is a summary")
expect(summaryMessage.isSummary).toBe(true)
@ -398,8 +395,7 @@ describe("summarizeConversation", () => {
)
// Should successfully summarize
// Result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
expect(result.messages.length).toBe(messages.length + 1) // Original + summary
expect(result.cost).toBe(0.03)
expect(result.summary).toBe("Concise summary")
expect(result.error).toBeUndefined()

View file

@ -100,11 +100,7 @@ export async function summarizeConversation(
)
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
// Always preserve the first message (which may contain slash command content)
const firstMessage = messages[0]
// Get messages to summarize, excluding the first message and last N messages
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(1, -N_MESSAGES_TO_KEEP))
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
if (messagesToSummarize.length <= 1) {
const error =
@ -188,8 +184,7 @@ export async function summarizeConversation(
isSummary: true,
}
// Reconstruct messages: [first message, summary, last N messages]
const newMessages = [firstMessage, summaryMessage, ...keepMessages]
const newMessages = [...messages.slice(0, -N_MESSAGES_TO_KEEP), summaryMessage, ...keepMessages]
// Count the tokens in the context for the next API request
// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens

View file

@ -845,19 +845,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const message = this.messageQueueService.dequeueMessage()
if (message) {
// Check if this is a tool approval ask that needs to be handled
if (
type === "tool" ||
type === "command" ||
type === "browser_action_launch" ||
type === "use_mcp_server"
) {
// For tool approvals, we need to approve first, then send the message if there's text/images
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
} else {
// For other ask types (like followup), fulfill the ask directly
this.setMessageResponse(message.text, message.images)
}
setTimeout(async () => {
await this.submitUserMessage(message.text, message.images)
}, 0)
}
}
@ -2908,28 +2898,4 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
public get cwd() {
return this.workspacePath
}
/**
* Process any queued messages by dequeuing and submitting them.
* This ensures that queued user messages are sent when appropriate,
* preventing them from getting stuck in the queue.
*
* @param context - Context string for logging (e.g., the calling tool name)
*/
public processQueuedMessages(): void {
try {
if (!this.messageQueueService.isEmpty()) {
const queued = this.messageQueueService.dequeueMessage()
if (queued) {
setTimeout(() => {
this.submitUserMessage(queued.text, queued.images).catch((err) =>
console.error(`[Task] Failed to submit queued message:`, err),
)
}, 0)
}
}
} catch (e) {
console.error(`[Task] Queue processing error:`, e)
}
}
}

View file

@ -40,7 +40,6 @@ describe("applyDiffTool experiment routing", () => {
api: {
getModel: vi.fn().mockReturnValue({ id: "test-model" }),
},
processQueuedMessages: vi.fn(),
} as any
mockBlock = {

View file

@ -163,60 +163,6 @@ describe("generateImageTool", () => {
expect(mockGenerateImage).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalled()
})
it("should add cache-busting parameter to image URI", async () => {
const completeBlock: ToolUse = {
type: "tool_use",
name: "generate_image",
params: {
prompt: "Generate a test image",
path: "test-image.png",
},
partial: false,
}
// Mock convertToWebviewUri to return a test URI
const mockWebviewUri = "https://file+.vscode-resource.vscode-cdn.net/test/workspace/test-image.png"
mockCline.providerRef.deref().convertToWebviewUri = vi.fn().mockReturnValue(mockWebviewUri)
// Mock the OpenRouterHandler generateImage method
const mockGenerateImage = vi.fn().mockResolvedValue({
success: true,
imageData: "data:image/png;base64,fakebase64data",
})
vi.mocked(OpenRouterHandler).mockImplementation(
() =>
({
generateImage: mockGenerateImage,
}) as any,
)
await generateImageTool(
mockCline as Task,
completeBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Check that cline.say was called with image data containing cache-busting parameter
expect(mockCline.say).toHaveBeenCalledWith("image", expect.stringMatching(/"imageUri":"[^"]+\?t=\d+"/))
// Verify the imageUri contains the cache-busting parameter
const sayCall = mockCline.say.mock.calls.find((call: any[]) => call[0] === "image")
if (sayCall) {
const imageData = JSON.parse(sayCall[1])
expect(imageData.imageUri).toMatch(/\?t=\d+$/)
// Handle both Unix and Windows path separators
const expectedPath =
process.platform === "win32"
? "\\test\\workspace\\test-image.png"
: "/test/workspace/test-image.png"
expect(imageData.imagePath).toBe(expectedPath)
}
})
})
describe("missing parameters", () => {

View file

@ -91,7 +91,6 @@ describe("multiApplyDiffTool", () => {
trackFileContext: vi.fn().mockResolvedValue(undefined),
},
didEditFile: false,
processQueuedMessages: vi.fn(),
} as any
mockAskApproval = vi.fn().mockResolvedValue(true)

View file

@ -207,7 +207,6 @@ export async function applyDiffToolLegacy(
if (!didApprove) {
await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view
cline.processQueuedMessages()
return
}
@ -246,15 +245,11 @@ export async function applyDiffToolLegacy(
await cline.diffViewProvider.reset()
// Process any queued messages after file edit completes
cline.processQueuedMessages()
return
}
} catch (error) {
await handleError("applying diff", error)
await cline.diffViewProvider.reset()
cline.processQueuedMessages()
return
}
}

View file

@ -244,11 +244,7 @@ export async function generateImageTool(
const fullImagePath = path.join(cline.cwd, finalPath)
// Convert to webview URI if provider is available
let imageUri = provider?.convertToWebviewUri?.(fullImagePath) ?? vscode.Uri.file(fullImagePath).toString()
// Add cache-busting parameter to prevent browser caching issues
const cacheBuster = Date.now()
imageUri = imageUri.includes("?") ? `${imageUri}&t=${cacheBuster}` : `${imageUri}?t=${cacheBuster}`
const imageUri = provider?.convertToWebviewUri?.(fullImagePath) ?? vscode.Uri.file(fullImagePath).toString()
// Send the image with the webview URI
await cline.say("image", JSON.stringify({ imageUri, imagePath: fullImagePath }))

View file

@ -187,9 +187,6 @@ export async function insertContentTool(
pushToolResult(message)
await cline.diffViewProvider.reset()
// Process any queued messages after file edit completes
cline.processQueuedMessages()
} catch (error) {
handleError("insert content", error)
await cline.diffViewProvider.reset()

View file

@ -172,7 +172,6 @@ Original error: ${errorMessage}`
TelemetryService.instance.captureDiffApplicationError(cline.taskId, cline.consecutiveMistakeCount)
await cline.say("diff_error", `Failed to parse apply_diff XML: ${errorMessage}`)
pushToolResult(detailedError)
cline.processQueuedMessages()
return
}
} else if (legacyPath && typeof legacyDiffContent === "string") {
@ -196,7 +195,6 @@ Original error: ${errorMessage}`
"args (or legacy 'path' and 'diff' parameters)",
)
pushToolResult(errorMsg)
cline.processQueuedMessages()
return
}
@ -212,7 +210,6 @@ Original error: ${errorMessage}`
: "args (must contain at least one valid file element)",
),
)
cline.processQueuedMessages()
return
}
@ -678,12 +675,10 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
// Push the final result combining all operation results
pushToolResult(results.join("\n\n") + singleBlockNotice)
cline.processQueuedMessages()
return
} catch (error) {
await handleError("applying diff", error)
await cline.diffViewProvider.reset()
cline.processQueuedMessages()
return
}
}

View file

@ -263,9 +263,6 @@ export async function searchAndReplaceTool(
// Record successful tool usage and cleanup
cline.recordToolUsage("search_and_replace")
await cline.diffViewProvider.reset()
// Process any queued messages after file edit completes
cline.processQueuedMessages()
} catch (error) {
handleError("search and replace", error)
await cline.diffViewProvider.reset()

View file

@ -308,9 +308,6 @@ export async function writeToFileTool(
await cline.diffViewProvider.reset()
// Process any queued messages after file edit completes
cline.processQueuedMessages()
return
}
} catch (error) {

View file

@ -141,7 +141,7 @@ export class ClineProvider
public isViewLaunched = false
public settingsImportedAt?: number
public readonly latestAnnouncementId = "sep-2025-roo-code-cloud" // Roo Code Cloud announcement
public readonly latestAnnouncementId = "aug-25-2025-grok-code-fast" // Update for Grok Code Fast announcement
public readonly providerSettingsManager: ProviderSettingsManager
public readonly customModesManager: CustomModesManager
@ -867,7 +867,7 @@ export class ClineProvider
fuzzyMatchThreshold,
experiments,
cloudUserInfo,
taskSyncEnabled,
remoteControlEnabled,
} = await this.getState()
const task = new Task({
@ -884,7 +884,7 @@ export class ClineProvider
taskNumber: historyItem.number,
workspacePath: historyItem.workspace,
onCreated: this.taskCreationCallback,
enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled),
enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled),
})
await this.addClineToStack(task)
@ -1777,12 +1777,10 @@ export class ClineProvider
includeDiagnosticMessages,
maxDiagnosticMessages,
includeTaskHistoryInEnhance,
taskSyncEnabled,
remoteControlEnabled,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
} = await this.getState()
const telemetryKey = process.env.POSTHOG_API_KEY
@ -1914,12 +1912,10 @@ export class ClineProvider
includeDiagnosticMessages: includeDiagnosticMessages ?? true,
maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
taskSyncEnabled,
remoteControlEnabled,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
}
}
@ -2007,16 +2003,6 @@ export class ClineProvider
)
}
let taskSyncEnabled: boolean = false
try {
taskSyncEnabled = CloudService.instance.isTaskSyncEnabled()
} catch (error) {
console.error(
`[getState] failed to get task sync enabled state: ${error instanceof Error ? error.message : String(error)}`,
)
}
// Return the same structure as before.
return {
apiConfiguration: providerSettings,
@ -2124,7 +2110,6 @@ export class ClineProvider
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
taskSyncEnabled,
remoteControlEnabled: (() => {
try {
const cloudSettings = CloudService.instance.getUserSettings()
@ -2138,18 +2123,6 @@ export class ClineProvider
})(),
openRouterImageApiKey: stateValues.openRouterImageApiKey,
openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
featureRoomoteControlEnabled: (() => {
try {
const userSettings = CloudService.instance.getUserSettings()
const hasOrganization = cloudUserInfo?.organizationId != null
return hasOrganization || (userSettings?.features?.roomoteControlEnabled ?? false)
} catch (error) {
console.error(
`[getState] failed to get featureRoomoteControlEnabled: ${error instanceof Error ? error.message : String(error)}`,
)
return false
}
})(),
}
}

View file

@ -554,9 +554,6 @@ describe("ClineProvider", () => {
diagnosticsEnabled: true,
openRouterImageApiKey: undefined,
openRouterImageGenerationSelectedModel: undefined,
remoteControlEnabled: false,
taskSyncEnabled: false,
featureRoomoteControlEnabled: false,
}
const message: ExtensionMessage = {
@ -1335,11 +1332,19 @@ describe("ClineProvider", () => {
text: "Edited message content",
})
// Verify correct messages were kept - delete from the preceding user message to truly replace it
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([])
// Verify correct messages were kept (only messages before the edited one)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[2],
])
// Verify correct API messages were kept
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([])
// Verify correct API messages were kept (only messages before the edited one)
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[2],
])
// The new flow calls webviewMessageHandler recursively with askResponse
// We need to verify the recursive call happened by checking if the handler was called again
@ -3011,7 +3016,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
mockCline.overwriteClineMessages = vi.fn()
mockCline.overwriteApiConversationHistory = vi.fn()
mockCline.submitUserMessage = vi.fn()
mockCline.handleWebviewAskResponse = vi.fn()
await provider.addClineToStack(mockCline)
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@ -3041,11 +3046,9 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
text: "Edited message with preserved images",
})
// Verify messages were edited correctly - the ORIGINAL user message and all subsequent messages are removed
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
// Verify submitUserMessage was called with the edited content
expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with preserved images", undefined)
// Verify messages were edited correctly - messages up to the edited message should remain
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
})
test("handles editing messages with file attachments", async () => {
@ -3067,7 +3070,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
mockCline.overwriteClineMessages = vi.fn()
mockCline.overwriteApiConversationHistory = vi.fn()
mockCline.submitUserMessage = vi.fn()
mockCline.handleWebviewAskResponse = vi.fn()
await provider.addClineToStack(mockCline)
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@ -3098,7 +3101,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
})
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with file attachment", undefined)
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
"messageResponse",
"Edited message with file attachment",
undefined,
)
})
})
@ -3190,7 +3197,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
// The error should be caught and shown
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Connection lost")
})
})
@ -3313,7 +3320,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
text: "Edited message",
})
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized")
})
describe("Malformed Requests and Invalid Formats", () => {
@ -3537,7 +3544,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
// Verify cleanup was attempted before failure
expect(cleanupSpy).toHaveBeenCalled()
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed")
})
test("validates proper cleanup during failed delete operations", async () => {
@ -3577,7 +3584,9 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
// Verify cleanup was attempted before failure
expect(cleanupSpy).toHaveBeenCalled()
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_deleting_message")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Error deleting message: Delete operation failed",
)
})
})
@ -3600,7 +3609,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
mockCline.overwriteClineMessages = vi.fn()
mockCline.overwriteApiConversationHistory = vi.fn()
mockCline.submitUserMessage = vi.fn()
mockCline.handleWebviewAskResponse = vi.fn()
await provider.addClineToStack(mockCline)
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@ -3629,7 +3638,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
expect(mockCline.submitUserMessage).toHaveBeenCalledWith(largeEditedContent, undefined)
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
"messageResponse",
largeEditedContent,
undefined,
)
})
test("handles deleting messages with large payloads", async () => {
@ -3809,7 +3822,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
] as any[]
mockCline.overwriteClineMessages = vi.fn()
mockCline.overwriteApiConversationHistory = vi.fn()
mockCline.submitUserMessage = vi.fn()
mockCline.handleWebviewAskResponse = vi.fn()
await provider.addClineToStack(mockCline)
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@ -3842,7 +3855,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
// Should handle future timestamps correctly
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
expect(mockCline.submitUserMessage).toHaveBeenCalled()
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled()
})
})
})

View file

@ -1,245 +0,0 @@
import { describe, it, expect, beforeEach, vi } from "vitest"
import { webviewMessageHandler } from "../webviewMessageHandler"
import * as vscode from "vscode"
import { ClineProvider } from "../ClineProvider"
// Mock the saveTaskMessages function
vi.mock("../../task-persistence", () => ({
saveTaskMessages: vi.fn(),
}))
// Mock the i18n module
vi.mock("../../../i18n", () => ({
t: vi.fn((key: string) => key),
changeLanguage: vi.fn(),
}))
vi.mock("vscode", () => ({
window: {
showErrorMessage: vi.fn(),
showWarningMessage: vi.fn(),
showInformationMessage: vi.fn(),
},
workspace: {
workspaceFolders: undefined,
getConfiguration: vi.fn(() => ({
get: vi.fn(),
update: vi.fn(),
})),
},
ConfigurationTarget: {
Global: 1,
Workspace: 2,
WorkspaceFolder: 3,
},
Uri: {
parse: vi.fn((str) => ({ toString: () => str })),
file: vi.fn((path) => ({ fsPath: path })),
},
env: {
openExternal: vi.fn(),
clipboard: {
writeText: vi.fn(),
},
},
commands: {
executeCommand: vi.fn(),
},
}))
describe("webviewMessageHandler delete functionality", () => {
let provider: any
let getCurrentTaskMock: any
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks()
// Create mock task
getCurrentTaskMock = {
clineMessages: [],
apiConversationHistory: [],
overwriteClineMessages: vi.fn(async () => {}),
overwriteApiConversationHistory: vi.fn(async () => {}),
taskId: "test-task-id",
}
// Create mock provider
provider = {
getCurrentTask: vi.fn(() => getCurrentTaskMock),
postMessageToWebview: vi.fn(),
contextProxy: {
getValue: vi.fn(),
setValue: vi.fn(async () => {}),
globalStorageUri: { fsPath: "/test/path" },
},
log: vi.fn(),
cwd: "/test/cwd",
}
})
describe("handleDeleteMessageConfirm", () => {
it("should handle deletion when apiConversationHistoryIndex is -1 (message not in API history)", async () => {
// Setup test data with a user message and assistant response
const userMessageTs = 1000
const assistantMessageTs = 1001
getCurrentTaskMock.clineMessages = [
{ ts: userMessageTs, say: "user", text: "Hello" },
{ ts: assistantMessageTs, say: "assistant", text: "Hi there" },
]
// API history has the assistant message but not the user message
// This simulates the case where the user message wasn't in API history
getCurrentTaskMock.apiConversationHistory = [
{ ts: assistantMessageTs, role: "assistant", content: { type: "text", text: "Hi there" } },
{
ts: 1002,
role: "assistant",
content: { type: "text", text: "attempt_completion" },
name: "attempt_completion",
},
]
// Call delete for the user message
await webviewMessageHandler(provider, {
type: "deleteMessageConfirm",
messageTs: userMessageTs,
})
// Verify that clineMessages was truncated at the correct index
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([])
// When message is not found in API history (index is -1),
// API history should be truncated from the first API message at/after the deleted timestamp (fallback)
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([])
})
it("should handle deletion when exact apiConversationHistoryIndex is found", async () => {
// Setup test data where message exists in both arrays
const messageTs = 1000
getCurrentTaskMock.clineMessages = [
{ ts: 900, say: "user", text: "Previous message" },
{ ts: messageTs, say: "user", text: "Delete this" },
{ ts: 1100, say: "assistant", text: "Response" },
]
getCurrentTaskMock.apiConversationHistory = [
{ ts: 900, role: "user", content: { type: "text", text: "Previous message" } },
{ ts: messageTs, role: "user", content: { type: "text", text: "Delete this" } },
{ ts: 1100, role: "assistant", content: { type: "text", text: "Response" } },
]
// Call delete
await webviewMessageHandler(provider, {
type: "deleteMessageConfirm",
messageTs: messageTs,
})
// Verify truncation at correct indices
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([
{ ts: 900, say: "user", text: "Previous message" },
])
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([
{ ts: 900, role: "user", content: { type: "text", text: "Previous message" } },
])
})
it("should handle deletion when message not found in clineMessages", async () => {
getCurrentTaskMock.clineMessages = [{ ts: 1000, say: "user", text: "Some message" }]
getCurrentTaskMock.apiConversationHistory = []
// Call delete with non-existent timestamp
await webviewMessageHandler(provider, {
type: "deleteMessageConfirm",
messageTs: 9999,
})
// Verify error message was shown (expecting translation key since t() is mocked to return the key)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.message.message_not_found")
// Verify no truncation occurred
expect(getCurrentTaskMock.overwriteClineMessages).not.toHaveBeenCalled()
expect(getCurrentTaskMock.overwriteApiConversationHistory).not.toHaveBeenCalled()
})
it("should handle deletion with attempt_completion in API history", async () => {
// Setup test data with attempt_completion
const userMessageTs = 1000
const attemptCompletionTs = 1001
getCurrentTaskMock.clineMessages = [
{ ts: userMessageTs, say: "user", text: "Fix the bug" },
{ ts: attemptCompletionTs, say: "assistant", text: "I've fixed the bug" },
]
// API history has attempt_completion but user message is missing
getCurrentTaskMock.apiConversationHistory = [
{
ts: attemptCompletionTs,
role: "assistant",
content: {
type: "text",
text: "I've fixed the bug in the code",
},
name: "attempt_completion",
},
{
ts: 1002,
role: "user",
content: { type: "text", text: "Looks good, but..." },
},
]
// Call delete for the user message
await webviewMessageHandler(provider, {
type: "deleteMessageConfirm",
messageTs: userMessageTs,
})
// Verify that clineMessages was truncated
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([])
// API history should be truncated from first message at/after deleted timestamp (fallback)
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([])
})
it("should preserve messages before the deleted one", async () => {
const messageTs = 2000
getCurrentTaskMock.clineMessages = [
{ ts: 1000, say: "user", text: "First message" },
{ ts: 1500, say: "assistant", text: "First response" },
{ ts: messageTs, say: "user", text: "Delete this" },
{ ts: 2500, say: "assistant", text: "Response to delete" },
]
getCurrentTaskMock.apiConversationHistory = [
{ ts: 1000, role: "user", content: { type: "text", text: "First message" } },
{ ts: 1500, role: "assistant", content: { type: "text", text: "First response" } },
{ ts: messageTs, role: "user", content: { type: "text", text: "Delete this" } },
{ ts: 2500, role: "assistant", content: { type: "text", text: "Response to delete" } },
]
await webviewMessageHandler(provider, {
type: "deleteMessageConfirm",
messageTs: messageTs,
})
// Should preserve messages before the deleted one
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([
{ ts: 1000, say: "user", text: "First message" },
{ ts: 1500, say: "assistant", text: "First response" },
])
// API history should be truncated at the exact index
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([
{ ts: 1000, role: "user", content: { type: "text", text: "First message" } },
{ ts: 1500, role: "assistant", content: { type: "text", text: "First response" } },
])
})
})
})

View file

@ -1,390 +0,0 @@
import type { Mock } from "vitest"
import { describe, it, expect, vi, beforeEach } from "vitest"
// Mock dependencies first
vi.mock("vscode", () => ({
window: {
showWarningMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn(),
update: vi.fn(),
}),
},
Uri: {
file: vi.fn((path) => ({ fsPath: path })),
},
env: {
uriScheme: "vscode",
},
}))
vi.mock("../../task-persistence", () => ({
saveTaskMessages: vi.fn(),
}))
vi.mock("../../../api/providers/fetchers/modelCache", () => ({
getModels: vi.fn(),
flushModels: vi.fn(),
}))
vi.mock("../checkpointRestoreHandler", () => ({
handleCheckpointRestoreOperation: vi.fn(),
}))
// Import after mocks
import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import type { ClineMessage } from "@roo-code/types"
import type { ApiMessage } from "../../task-persistence/apiMessages"
describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
let mockClineProvider: ClineProvider
let mockCurrentTask: any
beforeEach(() => {
vi.clearAllMocks()
// Create a mock task with messages
mockCurrentTask = {
taskId: "test-task-id",
clineMessages: [] as ClineMessage[],
apiConversationHistory: [] as ApiMessage[],
overwriteClineMessages: vi.fn(),
overwriteApiConversationHistory: vi.fn(),
handleWebviewAskResponse: vi.fn(),
}
// Create mock provider
mockClineProvider = {
getCurrentTask: vi.fn().mockReturnValue(mockCurrentTask),
postMessageToWebview: vi.fn(),
contextProxy: {
getValue: vi.fn(),
setValue: vi.fn(),
globalStorageUri: { fsPath: "/mock/storage" },
},
log: vi.fn(),
} as unknown as ClineProvider
})
it("should not modify API history when apiConversationHistoryIndex is -1", async () => {
// Setup: User message followed by attempt_completion
const userMessageTs = 1000
const assistantMessageTs = 2000
const completionMessageTs = 3000
// UI messages (clineMessages)
mockCurrentTask.clineMessages = [
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Hello",
} as ClineMessage,
{
ts: completionMessageTs,
type: "say",
say: "completion_result",
text: "Task Completed!",
} as ClineMessage,
]
// API conversation history - note the user message is missing (common scenario after condense)
mockCurrentTask.apiConversationHistory = [
{
ts: assistantMessageTs,
role: "assistant",
content: [
{
type: "text",
text: "I'll help you with that.",
},
],
},
{
ts: completionMessageTs,
role: "assistant",
content: [
{
type: "tool_use",
name: "attempt_completion",
id: "tool-1",
input: {
result: "Task Completed!",
},
},
],
},
] as ApiMessage[]
// Trigger edit confirmation
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Hello World", // edited content
restoreCheckpoint: false,
})
// Verify that UI messages were truncated at the correct index
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith(
[], // All messages before index 0 (empty array)
)
// API history should be truncated from first message at/after edited timestamp (fallback)
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
})
it("should preserve messages before the edited message when message not in API history", async () => {
const earlierMessageTs = 500
const userMessageTs = 1000
const assistantMessageTs = 2000
// UI messages
mockCurrentTask.clineMessages = [
{
ts: earlierMessageTs,
type: "say",
say: "user_feedback",
text: "Earlier message",
} as ClineMessage,
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Hello",
} as ClineMessage,
{
ts: assistantMessageTs,
type: "say",
say: "text",
text: "Response",
} as ClineMessage,
]
// API history - missing the exact user message at ts=1000
mockCurrentTask.apiConversationHistory = [
{
ts: earlierMessageTs,
role: "user",
content: [{ type: "text", text: "Earlier message" }],
},
{
ts: assistantMessageTs,
role: "assistant",
content: [{ type: "text", text: "Response" }],
},
] as ApiMessage[]
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Hello World",
restoreCheckpoint: false,
})
// Verify UI messages were truncated to preserve earlier message
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([
{
ts: earlierMessageTs,
type: "say",
say: "user_feedback",
text: "Earlier message",
},
])
// API history should be truncated from the first API message at/after the edited timestamp (fallback)
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
{
ts: earlierMessageTs,
role: "user",
content: [{ type: "text", text: "Earlier message" }],
},
])
})
it("should not use fallback when exact apiConversationHistoryIndex is found", async () => {
const userMessageTs = 1000
const assistantMessageTs = 2000
// Both UI and API have the message at the same timestamp
mockCurrentTask.clineMessages = [
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Hello",
} as ClineMessage,
{
ts: assistantMessageTs,
type: "say",
say: "text",
text: "Response",
} as ClineMessage,
]
mockCurrentTask.apiConversationHistory = [
{
ts: userMessageTs,
role: "user",
content: [{ type: "text", text: "Hello" }],
},
{
ts: assistantMessageTs,
role: "assistant",
content: [{ type: "text", text: "Response" }],
},
] as ApiMessage[]
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Hello World",
restoreCheckpoint: false,
})
// Both should be truncated at index 0
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
})
it("should handle case where no API messages match timestamp criteria", async () => {
const userMessageTs = 3000
mockCurrentTask.clineMessages = [
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Hello",
} as ClineMessage,
]
// All API messages have timestamps before the edited message
mockCurrentTask.apiConversationHistory = [
{
ts: 1000,
role: "assistant",
content: [{ type: "text", text: "Old message 1" }],
},
{
ts: 2000,
role: "assistant",
content: [{ type: "text", text: "Old message 2" }],
},
] as ApiMessage[]
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Hello World",
restoreCheckpoint: false,
})
// UI messages truncated
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
// API history should not be modified when no API messages meet the timestamp criteria
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
})
it("should handle empty API conversation history gracefully", async () => {
const userMessageTs = 1000
mockCurrentTask.clineMessages = [
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Hello",
} as ClineMessage,
]
mockCurrentTask.apiConversationHistory = []
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Hello World",
restoreCheckpoint: false,
})
// UI messages should be truncated
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
// API history should not be modified when message not found
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
})
it("should correctly handle attempt_completion in API history", async () => {
const userMessageTs = 1000
const completionTs = 2000
const feedbackTs = 3000
mockCurrentTask.clineMessages = [
{
ts: userMessageTs,
type: "say",
say: "user_feedback",
text: "Do something",
} as ClineMessage,
{
ts: completionTs,
type: "say",
say: "completion_result",
text: "Task Completed!",
} as ClineMessage,
{
ts: feedbackTs,
type: "say",
say: "user_feedback",
text: "Thanks",
} as ClineMessage,
]
// API history with attempt_completion tool use (user message missing)
mockCurrentTask.apiConversationHistory = [
{
ts: completionTs,
role: "assistant",
content: [
{
type: "tool_use",
name: "attempt_completion",
id: "tool-1",
input: {
result: "Task Completed!",
},
},
],
},
{
ts: feedbackTs,
role: "user",
content: [
{
type: "text",
text: "Thanks",
},
],
},
] as ApiMessage[]
// Edit the first user message
await webviewMessageHandler(mockClineProvider, {
type: "editMessageConfirm",
messageTs: userMessageTs,
text: "Do something else",
restoreCheckpoint: false,
})
// UI messages truncated at edited message
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
// API history should be truncated from first message at/after edited timestamp (fallback)
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
})
})

View file

@ -136,48 +136,6 @@ describe("webviewMessageHandler - requestLmStudioModels", () => {
})
})
describe("webviewMessageHandler - requestOllamaModels", () => {
beforeEach(() => {
vi.clearAllMocks()
mockClineProvider.getState = vi.fn().mockResolvedValue({
apiConfiguration: {
ollamaModelId: "model-1",
ollamaBaseUrl: "http://localhost:1234",
},
})
})
it("successfully fetches models from Ollama", async () => {
const mockModels: ModelRecord = {
"model-1": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "Test model 1",
},
"model-2": {
maxTokens: 8192,
contextWindow: 16384,
supportsPromptCache: false,
description: "Test model 2",
},
}
mockGetModels.mockResolvedValue(mockModels)
await webviewMessageHandler(mockClineProvider, {
type: "requestOllamaModels",
})
expect(mockGetModels).toHaveBeenCalledWith({ provider: "ollama", baseUrl: "http://localhost:1234" })
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "ollamaModels",
ollamaModels: mockModels,
})
})
})
describe("webviewMessageHandler - requestRouterModels", () => {
beforeEach(() => {
vi.clearAllMocks()

View file

@ -11,7 +11,6 @@ import {
type ClineMessage,
type TelemetrySetting,
TelemetryEventName,
UserSettingsConfig,
} from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
import { TelemetryService } from "@roo-code/telemetry"
@ -85,17 +84,6 @@ export const webviewMessageHandler = async (
return { messageIndex, apiConversationHistoryIndex }
}
/**
* Fallback: find first API history index at or after a timestamp.
* Used when the exact user message isn't present in apiConversationHistory (e.g., after condense).
*/
const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => {
if (typeof ts !== "number") return -1
return currentCline.apiConversationHistory.findIndex(
(msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
)
}
/**
* Removes the target message and all subsequent messages
*/
@ -121,20 +109,18 @@ export const webviewMessageHandler = async (
// Check if there's a checkpoint before this message
const currentCline = provider.getCurrentTask()
let hasCheckpoint = false
if (currentCline) {
const { messageIndex } = findMessageIndices(messageTs, currentCline)
if (messageIndex !== -1) {
// Find the last checkpoint before this message
const checkpoints = currentCline.clineMessages.filter(
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
)
if (!currentCline) {
await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
return
}
const { messageIndex } = findMessageIndices(messageTs, currentCline)
if (messageIndex !== -1) {
// Find the last checkpoint before this message
const checkpoints = currentCline.clineMessages.filter(
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
)
hasCheckpoint = checkpoints.length > 0
hasCheckpoint = checkpoints.length > 0
} else {
console.log("[webviewMessageHandler] Message not found! Looking for ts:", messageTs)
}
}
// Send message to webview to show delete confirmation dialog
@ -156,15 +142,11 @@ export const webviewMessageHandler = async (
}
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
// Determine API truncation index with timestamp fallback if exact match not found
let apiIndexToUse = apiConversationHistoryIndex
const tsThreshold = currentCline.clineMessages[messageIndex]?.ts
if (apiIndexToUse === -1 && typeof tsThreshold === "number") {
apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline)
}
if (messageIndex === -1) {
await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs }))
const errorMessage = `Message with timestamp ${messageTs} not found`
console.error("[handleDeleteMessageConfirm]", errorMessage)
await vscode.window.showErrorMessage(errorMessage)
return
}
@ -206,7 +188,7 @@ export const webviewMessageHandler = async (
}
// Delete this message and all subsequent messages
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse)
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
// Restore checkpoint associations for preserved messages
for (const [ts, checkpoint] of preservedCheckpoints) {
@ -222,16 +204,11 @@ export const webviewMessageHandler = async (
taskId: currentCline.taskId,
globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
})
// Update the UI to reflect the deletion
await provider.postStateToWebview()
}
} catch (error) {
console.error("Error in delete message:", error)
vscode.window.showErrorMessage(
t("common:errors.message.error_deleting_message", {
error: error instanceof Error ? error.message : String(error),
}),
`Error deleting message: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
@ -288,7 +265,7 @@ export const webviewMessageHandler = async (
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
if (messageIndex === -1) {
const errorMessage = t("common:errors.message.message_not_found", { messageTs })
const errorMessage = `Message with timestamp ${messageTs} not found`
console.error("[handleEditMessageConfirm]", errorMessage)
await vscode.window.showErrorMessage(errorMessage)
return
@ -331,49 +308,18 @@ export const webviewMessageHandler = async (
}
}
// For non-checkpoint edits, remove the ORIGINAL user message being edited and all subsequent messages
// Determine the correct starting index to delete from (prefer the last preceding user_feedback message)
let deleteFromMessageIndex = messageIndex
let deleteFromApiIndex = apiConversationHistoryIndex
// Find the nearest preceding user message to ensure we replace the original, not just the assistant reply
for (let i = messageIndex; i >= 0; i--) {
const m = currentCline.clineMessages[i]
if (m?.say === "user_feedback") {
deleteFromMessageIndex = i
// Align API history truncation to the same user message timestamp if present
const userTs = m.ts
if (typeof userTs === "number") {
const apiIdx = currentCline.apiConversationHistory.findIndex(
(am: ApiMessage) => am.ts === userTs,
)
if (apiIdx !== -1) {
deleteFromApiIndex = apiIdx
}
}
break
}
}
// Timestamp fallback for API history when exact user message isn't present
if (deleteFromApiIndex === -1) {
const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts
if (typeof tsThresholdForEdit === "number") {
deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline)
}
}
// For non-checkpoint edits, preserve checkpoint associations for remaining messages
// Store checkpoints from messages that will be preserved
const preservedCheckpoints = new Map<number, any>()
for (let i = 0; i < deleteFromMessageIndex; i++) {
for (let i = 0; i < messageIndex; i++) {
const msg = currentCline.clineMessages[i]
if (msg?.checkpoint && msg.ts) {
preservedCheckpoints.set(msg.ts, msg.checkpoint)
}
}
// Delete the original (user) message and all subsequent messages
await removeMessagesThisAndSubsequent(currentCline, deleteFromMessageIndex, deleteFromApiIndex)
// Edit this message and delete subsequent
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
// Restore checkpoint associations for preserved messages
for (const [ts, checkpoint] of preservedCheckpoints) {
@ -390,16 +336,20 @@ export const webviewMessageHandler = async (
globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
})
// Update the UI to reflect the deletion
await provider.postStateToWebview()
// Process the edited message as a regular user message
webviewMessageHandler(provider, {
type: "askResponse",
askResponse: "messageResponse",
text: editedContent,
images,
})
await currentCline.submitUserMessage(editedContent, images)
// Don't initialize with history item for edit operations
// The webviewMessageHandler will handle the conversation state
} catch (error) {
console.error("Error in edit message:", error)
vscode.window.showErrorMessage(
t("common:errors.message.error_editing_message", {
error: error instanceof Error ? error.message : String(error),
}),
`Error editing message: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
@ -490,10 +440,10 @@ export const webviewMessageHandler = async (
),
)
// Enable telemetry by default (when unset) or when explicitly enabled
// If user already opted in to telemetry, enable telemetry service
provider.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
TelemetryService.instance.updateTelemetryState(isOptedIn)
})
@ -847,7 +797,7 @@ export const webviewMessageHandler = async (
if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
provider.postMessageToWebview({
type: "ollamaModels",
ollamaModels: result.value.models,
ollamaModels: Object.keys(result.value.models),
})
} else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) {
provider.postMessageToWebview({
@ -892,7 +842,7 @@ export const webviewMessageHandler = async (
if (Object.keys(ollamaModels).length > 0) {
provider.postMessageToWebview({
type: "ollamaModels",
ollamaModels: ollamaModels,
ollamaModels: Object.keys(ollamaModels),
})
}
} catch (error) {
@ -1189,21 +1139,16 @@ export const webviewMessageHandler = async (
`CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`,
)
}
break
case "taskSyncEnabled":
const enabled = message.bool ?? false
const updatedSettings: Partial<UserSettingsConfig> = {
taskSyncEnabled: enabled,
}
// If disabling task sync, also disable remote control
if (!enabled) {
updatedSettings.extensionBridgeEnabled = false
}
try {
await CloudService.instance.updateUserSettings(updatedSettings)
await provider.remoteControlEnabled(message.bool ?? false)
} catch (error) {
provider.log(`Failed to update cloud settings for task sync: ${error}`)
provider.log(
`ClineProvider#remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
)
}
await provider.postStateToWebview()
break
case "refreshAllMcpServers": {
const mcpHub = provider.getMcpHub()
@ -1506,17 +1451,9 @@ export const webviewMessageHandler = async (
}
break
case "deleteMessage": {
if (!provider.getCurrentTask()) {
await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
break
if (provider.getCurrentTask() && typeof message.value === "number" && message.value) {
await handleMessageModificationsOperation(message.value, "delete")
}
if (typeof message.value !== "number" || !message.value) {
await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_deletion"))
break
}
await handleMessageModificationsOperation(message.value, "delete")
break
}
case "submitEditedMessage": {
@ -1904,17 +1841,9 @@ export const webviewMessageHandler = async (
}
break
case "deleteMessageConfirm":
if (!message.messageTs) {
await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp"))
break
if (message.messageTs) {
await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint)
}
if (typeof message.messageTs !== "number") {
await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_invalid_timestamp"))
break
}
await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint)
break
case "editMessageConfirm":
if (message.messageTs && message.text) {
@ -2289,7 +2218,7 @@ export const webviewMessageHandler = async (
case "telemetrySetting": {
const telemetrySetting = message.text as TelemetrySetting
await updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
TelemetryService.instance.updateTelemetryState(isOptedIn)
await provider.postStateToWebview()
break
@ -2322,48 +2251,6 @@ export const webviewMessageHandler = async (
break
}
case "rooCloudManualUrl": {
try {
if (!message.text) {
vscode.window.showErrorMessage(t("common:errors.manual_url_empty"))
break
}
// Parse the callback URL to extract parameters
const callbackUrl = message.text.trim()
const uri = vscode.Uri.parse(callbackUrl)
if (!uri.query) {
throw new Error(t("common:errors.manual_url_no_query"))
}
const query = new URLSearchParams(uri.query)
const code = query.get("code")
const state = query.get("state")
const organizationId = query.get("organizationId")
if (!code || !state) {
throw new Error(t("common:errors.manual_url_missing_params"))
}
// Reuse the existing authentication flow
await CloudService.instance.handleAuthCallback(
code,
state,
organizationId === "null" ? null : organizationId,
)
await provider.postStateToWebview()
} catch (error) {
provider.log(`ManualUrl#handleAuthCallback failed: ${error}`)
const errorMessage = error instanceof Error ? error.message : t("common:errors.manual_url_auth_failed")
// Show error message through VS Code UI
vscode.window.showErrorMessage(`${t("common:errors.manual_url_auth_error")}: ${errorMessage}`)
}
break
}
case "saveCodeIndexSettingsAtomic": {
if (!message.codeIndexSettings) {
@ -3004,39 +2891,5 @@ export const webviewMessageHandler = async (
break
}
case "dismissUpsell": {
if (message.upsellId) {
try {
// Get current list of dismissed upsells
const dismissedUpsells = getGlobalState("dismissedUpsells") || []
// Add the new upsell ID if not already present
let updatedList = dismissedUpsells
if (!dismissedUpsells.includes(message.upsellId)) {
updatedList = [...dismissedUpsells, message.upsellId]
await updateGlobalState("dismissedUpsells", updatedList)
}
// Send updated list back to webview (use the already computed updatedList)
await provider.postMessageToWebview({
type: "dismissedUpsells",
list: updatedList,
})
} catch (error) {
// Fail silently as per Bruno's comment - it's OK to fail silently in this case
provider.log(`Failed to dismiss upsell: ${error instanceof Error ? error.message : String(error)}`)
}
}
break
}
case "getDismissedUpsells": {
// Send the current list of dismissed upsells to the webview
const dismissedUpsells = getGlobalState("dismissedUpsells") || []
await provider.postMessageToWebview({
type: "dismissedUpsells",
list: dismissedUpsells,
})
break
}
}
}

View file

@ -133,7 +133,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (data.state === "logged-out") {
try {
await provider.remoteControlEnabled(false)
await BridgeOrchestrator.disconnect()
cloudLogger("[CloudService] BridgeOrchestrator disconnected on logout")
} catch (error) {
cloudLogger(
@ -148,7 +148,20 @@ export async function activate(context: vscode.ExtensionContext) {
if (userInfo && CloudService.instance.cloudAPI) {
try {
provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
const config = await CloudService.instance.cloudAPI.bridgeConfig()
const isCloudAgent =
typeof process.env.ROO_CODE_CLOUD_TOKEN === "string" && process.env.ROO_CODE_CLOUD_TOKEN.length > 0
const remoteControlEnabled = isCloudAgent
? true
: (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false)
await BridgeOrchestrator.connectOrDisconnect(userInfo, remoteControlEnabled, {
...config,
provider,
sessionId: vscode.env.sessionId,
})
} catch (error) {
cloudLogger(
`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on settings change: ${error instanceof Error ? error.message : String(error)}`,
@ -168,7 +181,20 @@ export async function activate(context: vscode.ExtensionContext) {
}
try {
provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
const config = await CloudService.instance.cloudAPI.bridgeConfig()
const isCloudAgent =
typeof process.env.ROO_CODE_CLOUD_TOKEN === "string" && process.env.ROO_CODE_CLOUD_TOKEN.length > 0
const remoteControlEnabled = isCloudAgent
? true
: (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false)
await BridgeOrchestrator.connectOrDisconnect(userInfo, remoteControlEnabled, {
...config,
provider,
sessionId: vscode.env.sessionId,
})
} catch (error) {
cloudLogger(
`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on user change: ${error instanceof Error ? error.message : String(error)}`,

View file

@ -33,7 +33,6 @@
"checkpoint_timeout": "S'ha esgotat el temps en intentar restaurar el punt de control.",
"checkpoint_failed": "Ha fallat la restauració del punt de control.",
"git_not_installed": "Git és necessari per a la funció de punts de control. Si us plau, instal·la Git per activar els punts de control.",
"nested_git_repos_warning": "Els punts de control estan deshabilitats perquè s'han detectat repositoris git niats a l'espai de treball. Per utilitzar punts de control, si us plau elimina o reubica els repositoris git niats.",
"no_workspace": "Si us plau, obre primer una carpeta de projecte",
"update_support_prompt": "Ha fallat l'actualització del missatge de suport",
"reset_support_prompt": "Ha fallat el restabliment del missatge de suport",
@ -91,15 +90,6 @@
"apiKeyModelPlanMismatch": "Les claus API i els plans de subscripció permeten models diferents. Assegura't que el model seleccionat estigui inclòs al teu pla.",
"notFound": "No s'ha trobat l'executable Claude Code '{{claudePath}}'.\n\nInstal·la Claude Code CLI:\n1. Visita {{installationUrl}} per descarregar Claude Code\n2. Segueix les instruccions d'instal·lació per al teu sistema operatiu\n3. Assegura't que la comanda 'claude' estigui disponible al teu PATH\n4. Alternativament, configura una ruta personalitzada a la configuració de Roo sota 'Ruta de Claude Code'\n\nError original: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "No hi ha cap tasca activa de la qual eliminar missatges",
"invalid_timestamp_for_deletion": "Marca de temps del missatge no vàlida per a l'eliminació",
"cannot_delete_missing_timestamp": "No es pot eliminar el missatge: falta la marca de temps",
"cannot_delete_invalid_timestamp": "No es pot eliminar el missatge: marca de temps no vàlida",
"message_not_found": "Missatge amb marca de temps {{messageTs}} no trobat",
"error_deleting_message": "Error eliminant missatge: {{error}}",
"error_editing_message": "Error editant missatge: {{error}}"
},
"gemini": {
"generate_stream": "Error del flux de context de generació de Gemini: {{error}}",
"generate_complete_prompt": "Error de finalització de Gemini: {{error}}",
@ -120,11 +110,6 @@
"api": {
"invalidKeyInvalidChars": "La clau API conté caràcters no vàlids."
},
"manual_url_empty": "Si us plau, introdueix una URL de callback vàlida",
"manual_url_no_query": "URL de callback no vàlida: falten paràmetres de consulta",
"manual_url_missing_params": "URL de callback no vàlida: falten paràmetres requerits (code i state)",
"manual_url_auth_failed": "Autenticació manual per URL ha fallat",
"manual_url_auth_error": "Autenticació fallida",
"mode_import_failed": "Ha fallat la importació del mode: {{error}}"
},
"warnings": {
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.",
"organization_mismatch": "Has d'estar autenticat amb el compte de Roo Code Cloud de la teva organització.",
"manual_url_empty": "Si us plau, introdueix una URL de callback vàlida",
"manual_url_no_query": "URL de callback no vàlida: falten paràmetres de consulta",
"manual_url_missing_params": "URL de callback no vàlida: falten paràmetres requerits (code i state)",
"manual_url_auth_failed": "Autenticació manual per URL ha fallat",
"manual_url_auth_error": "Autenticació fallida",
"verification_failed": "No s'ha pogut verificar l'autenticació de l'organització."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Zeitüberschreitung beim Versuch, den Checkpoint wiederherzustellen.",
"checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.",
"git_not_installed": "Git ist für die Checkpoint-Funktion erforderlich. Bitte installiere Git, um Checkpoints zu aktivieren.",
"nested_git_repos_warning": "Checkpoints sind deaktiviert, da verschachtelte Git-Repositories im Arbeitsbereich erkannt wurden. Um Checkpoints zu verwenden, entferne oder verschiebe bitte die verschachtelten Git-Repositories.",
"no_workspace": "Bitte öffne zuerst einen Projektordner",
"update_support_prompt": "Fehler beim Aktualisieren der Support-Nachricht",
"reset_support_prompt": "Fehler beim Zurücksetzen der Support-Nachricht",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist.",
"notFound": "Claude Code ausführbare Datei '{{claudePath}}' nicht gefunden.\n\nBitte installiere Claude Code CLI:\n1. Besuche {{installationUrl}} um Claude Code herunterzuladen\n2. Folge den Installationsanweisungen für dein Betriebssystem\n3. Stelle sicher, dass der 'claude' Befehl in deinem PATH verfügbar ist\n4. Alternativ konfiguriere einen benutzerdefinierten Pfad in den Roo-Einstellungen unter 'Claude Code Pfad'\n\nUrsprünglicher Fehler: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Keine aktive Aufgabe, aus der Nachrichten gelöscht werden können",
"invalid_timestamp_for_deletion": "Ungültiger Nachrichten-Zeitstempel zum Löschen",
"cannot_delete_missing_timestamp": "Nachricht kann nicht gelöscht werden: fehlender Zeitstempel",
"cannot_delete_invalid_timestamp": "Nachricht kann nicht gelöscht werden: ungültiger Zeitstempel",
"message_not_found": "Nachricht mit Zeitstempel {{messageTs}} nicht gefunden",
"error_deleting_message": "Fehler beim Löschen der Nachricht: {{error}}",
"error_editing_message": "Fehler beim Bearbeiten der Nachricht: {{error}}"
},
"gemini": {
"generate_stream": "Fehler beim Generieren des Kontext-Streams von Gemini: {{error}}",
"generate_complete_prompt": "Fehler bei der Vervollständigung durch Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API-Schlüssel enthält ungültige Zeichen."
},
"manual_url_empty": "Bitte gib eine gültige Callback-URL ein",
"manual_url_no_query": "Ungültige Callback-URL: Query-Parameter fehlen",
"manual_url_missing_params": "Ungültige Callback-URL: erforderliche Parameter (code und state) fehlen",
"manual_url_auth_failed": "Manuelle URL-Authentifizierung fehlgeschlagen",
"manual_url_auth_error": "Authentifizierung fehlgeschlagen"
}
},
"warnings": {
"no_terminal_content": "Kein Terminal-Inhalt ausgewählt",

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Timed out when attempting to restore checkpoint.",
"checkpoint_failed": "Failed to restore checkpoint.",
"git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.",
"nested_git_repos_warning": "Checkpoints are disabled because nested git repositories were detected in the workspace. To use checkpoints, please remove or relocate nested git repositories.",
"no_workspace": "Please open a project folder first",
"update_support_prompt": "Failed to update support prompt",
"reset_support_prompt": "Failed to reset support prompt",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "No active task to delete messages from",
"invalid_timestamp_for_deletion": "Invalid message timestamp for deletion",
"cannot_delete_missing_timestamp": "Cannot delete message: missing timestamp",
"cannot_delete_invalid_timestamp": "Cannot delete message: invalid timestamp",
"message_not_found": "Message with timestamp {{messageTs}} not found",
"error_deleting_message": "Error deleting message: {{error}}",
"error_editing_message": "Error editing message: {{error}}"
},
"gemini": {
"generate_stream": "Gemini generate context stream error: {{error}}",
"generate_complete_prompt": "Gemini completion error: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API key contains invalid characters."
},
"manual_url_empty": "Please enter a valid callback URL",
"manual_url_no_query": "Invalid callback URL: missing query parameters",
"manual_url_missing_params": "Invalid callback URL: missing required parameters (code and state)",
"manual_url_auth_failed": "Manual URL authentication failed",
"manual_url_auth_error": "Authentication failed"
}
},
"warnings": {
"no_terminal_content": "No terminal content selected",

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Se agotó el tiempo al intentar restaurar el punto de control.",
"checkpoint_failed": "Error al restaurar el punto de control.",
"git_not_installed": "Git es necesario para la función de puntos de control. Por favor, instala Git para activar los puntos de control.",
"nested_git_repos_warning": "Los puntos de control están deshabilitados porque se detectaron repositorios git anidados en el espacio de trabajo. Para usar puntos de control, por favor elimina o reubica los repositorios git anidados.",
"no_workspace": "Por favor, abre primero una carpeta de proyecto",
"update_support_prompt": "Error al actualizar el mensaje de soporte",
"reset_support_prompt": "Error al restablecer el mensaje de soporte",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan.",
"notFound": "Ejecutable de Claude Code '{{claudePath}}' no encontrado.\n\nPor favor instala Claude Code CLI:\n1. Visita {{installationUrl}} para descargar Claude Code\n2. Sigue las instrucciones de instalación para tu sistema operativo\n3. Asegúrate de que el comando 'claude' esté disponible en tu PATH\n4. Alternativamente, configura una ruta personalizada en la configuración de Roo bajo 'Ruta de Claude Code'\n\nError original: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "No hay tarea activa de la cual eliminar mensajes",
"invalid_timestamp_for_deletion": "Marca de tiempo del mensaje no válida para eliminación",
"cannot_delete_missing_timestamp": "No se puede eliminar el mensaje: falta marca de tiempo",
"cannot_delete_invalid_timestamp": "No se puede eliminar el mensaje: marca de tiempo no válida",
"message_not_found": "Mensaje con marca de tiempo {{messageTs}} no encontrado",
"error_deleting_message": "Error eliminando mensaje: {{error}}",
"error_editing_message": "Error editando mensaje: {{error}}"
},
"gemini": {
"generate_stream": "Error del stream de contexto de generación de Gemini: {{error}}",
"generate_complete_prompt": "Error de finalización de Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "La clave API contiene caracteres inválidos."
},
"manual_url_empty": "Por favor, introduce una URL de callback válida",
"manual_url_no_query": "URL de callback inválida: faltan parámetros de consulta",
"manual_url_missing_params": "URL de callback inválida: faltan parámetros requeridos (code y state)",
"manual_url_auth_failed": "Autenticación manual por URL falló",
"manual_url_auth_error": "Error de autenticación"
}
},
"warnings": {
"no_terminal_content": "No hay contenido de terminal seleccionado",

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Expiration du délai lors de la tentative de rétablissement du checkpoint.",
"checkpoint_failed": "Échec du rétablissement du checkpoint.",
"git_not_installed": "Git est requis pour la fonctionnalité des points de contrôle. Veuillez installer Git pour activer les points de contrôle.",
"nested_git_repos_warning": "Les points de contrôle sont désactivés car des dépôts git imbriqués ont été détectés dans l'espace de travail. Pour utiliser les points de contrôle, veuillez supprimer ou déplacer les dépôts git imbriqués.",
"no_workspace": "Veuillez d'abord ouvrir un espace de travail",
"update_support_prompt": "Erreur lors de la mise à jour du prompt de support",
"reset_support_prompt": "Erreur lors de la réinitialisation du prompt de support",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan.",
"notFound": "Exécutable Claude Code '{{claudePath}}' introuvable.\n\nVeuillez installer Claude Code CLI :\n1. Visitez {{installationUrl}} pour télécharger Claude Code\n2. Suivez les instructions d'installation pour votre système d'exploitation\n3. Assurez-vous que la commande 'claude' est disponible dans votre PATH\n4. Alternativement, configurez un chemin personnalisé dans les paramètres Roo sous 'Chemin de Claude Code'\n\nErreur originale : {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Aucune tâche active pour supprimer des messages",
"invalid_timestamp_for_deletion": "Horodatage du message invalide pour la suppression",
"cannot_delete_missing_timestamp": "Impossible de supprimer le message : horodatage manquant",
"cannot_delete_invalid_timestamp": "Impossible de supprimer le message : horodatage invalide",
"message_not_found": "Message avec horodatage {{messageTs}} introuvable",
"error_deleting_message": "Erreur lors de la suppression du message : {{error}}",
"error_editing_message": "Erreur lors de la modification du message : {{error}}"
},
"gemini": {
"generate_stream": "Erreur du flux de contexte de génération Gemini : {{error}}",
"generate_complete_prompt": "Erreur d'achèvement de Gemini : {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "La clé API contient des caractères invalides."
},
"manual_url_empty": "Veuillez entrer une URL de callback valide",
"manual_url_no_query": "URL de callback invalide : paramètres de requête manquants",
"manual_url_missing_params": "URL de callback invalide : paramètres requis manquants (code et state)",
"manual_url_auth_failed": "Authentification par URL manuelle échouée",
"manual_url_auth_error": "Échec de l'authentification"
}
},
"warnings": {
"no_terminal_content": "Aucun contenu de terminal sélectionné",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.",
"organization_mismatch": "Vous devez être authentifié avec le compte Roo Code Cloud de votre organisation.",
"manual_url_empty": "Veuillez entrer une URL de callback valide",
"manual_url_no_query": "URL de callback invalide : paramètres de requête manquants",
"manual_url_missing_params": "URL de callback invalide : paramètres requis manquants (code et state)",
"manual_url_auth_failed": "Authentification par URL manuelle échouée",
"manual_url_auth_error": "Échec de l'authentification",
"verification_failed": "Impossible de vérifier l'authentification de l'organisation."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।",
"checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।",
"git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट সক্ষম करने के लिए Git इंस्टॉल करें।",
"nested_git_repos_warning": "चेकपॉइंट अक्षम हैं क्योंकि वर्कस्पेस में नेस्टेड git रिपॉजिटरी का पता चला है। चेकपॉइंट का उपयोग करने के लिए, कृपया नेस्टेड git रिपॉजिटरी को हटाएं या स्थानांतरित करें।",
"no_workspace": "कृपया पहले प्रोजेक्ट फ़ोल्डर खोलें",
"update_support_prompt": "सपोर्ट प्रॉम्प्ट अपडेट करने में विफल",
"reset_support_prompt": "सपोर्ट प्रॉम्प्ट रीसेट करने में विफल",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "संदेशों को हटाने के लिए कोई सक्रिय कार्य नहीं",
"invalid_timestamp_for_deletion": "हटाने के लिए अमान्य संदेश टाइमस्टैम्प",
"cannot_delete_missing_timestamp": "संदेश हटाया नहीं जा सकता: टाइमस्टैम्प गुम है",
"cannot_delete_invalid_timestamp": "संदेश हटाया नहीं जा सकता: अमान्य टाइमस्टैम्प",
"message_not_found": "टाइमस्टैम्प {{messageTs}} वाला संदेश नहीं मिला",
"error_deleting_message": "संदेश हटाने में त्रुटि: {{error}}",
"error_editing_message": "संदेश संपादित करने में त्रुटि: {{error}}"
},
"gemini": {
"generate_stream": "जेमिनी जनरेट कॉन्टेक्स्ट स्ट्रीम त्रुटि: {{error}}",
"generate_complete_prompt": "जेमिनी समापन त्रुटि: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।"
},
"manual_url_empty": "कृपया एक वैध callback URL दर्ज करें",
"manual_url_no_query": "अवैध callback URL: क्वेरी पैरामीटर गुम हैं",
"manual_url_missing_params": "अवैध callback URL: आवश्यक पैरामीटर गुम हैं (code और state)",
"manual_url_auth_failed": "मैनुअल URL प्रमाणीकरण असफल",
"manual_url_auth_error": "प्रमाणीकरण असफल"
}
},
"warnings": {
"no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",
"organization_mismatch": "आपको अपने संगठन के Roo Code Cloud खाते से प्रमाणित होना होगा।",
"manual_url_empty": "कृपया एक वैध callback URL दर्ज करें",
"manual_url_no_query": "अवैध callback URL: क्वेरी पैरामीटर गुम हैं",
"manual_url_missing_params": "अवैध callback URL: आवश्यक पैरामीटर गुम हैं (code और state)",
"manual_url_auth_failed": "मैनुअल URL प्रमाणीकरण असफल",
"manual_url_auth_error": "प्रमाणीकरण असफल",
"verification_failed": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।"
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Timeout saat mencoba memulihkan checkpoint.",
"checkpoint_failed": "Gagal memulihkan checkpoint.",
"git_not_installed": "Git diperlukan untuk fitur checkpoint. Silakan instal Git untuk mengaktifkan checkpoint.",
"nested_git_repos_warning": "Checkpoint dinonaktifkan karena repositori git bersarang terdeteksi di workspace. Untuk menggunakan checkpoint, silakan hapus atau pindahkan repositori git bersarang.",
"no_workspace": "Silakan buka folder proyek terlebih dahulu",
"update_support_prompt": "Gagal memperbarui support prompt",
"reset_support_prompt": "Gagal mereset support prompt",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Tidak ada tugas aktif untuk menghapus pesan",
"invalid_timestamp_for_deletion": "Timestamp pesan tidak valid untuk penghapusan",
"cannot_delete_missing_timestamp": "Tidak dapat menghapus pesan: timestamp tidak ada",
"cannot_delete_invalid_timestamp": "Tidak dapat menghapus pesan: timestamp tidak valid",
"message_not_found": "Pesan dengan timestamp {{messageTs}} tidak ditemukan",
"error_deleting_message": "Error menghapus pesan: {{error}}",
"error_editing_message": "Error mengedit pesan: {{error}}"
},
"gemini": {
"generate_stream": "Kesalahan aliran konteks pembuatan Gemini: {{error}}",
"generate_complete_prompt": "Kesalahan penyelesaian Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "Kunci API mengandung karakter tidak valid."
},
"manual_url_empty": "Silakan masukkan URL callback yang valid",
"manual_url_no_query": "URL callback tidak valid: parameter query hilang",
"manual_url_missing_params": "URL callback tidak valid: parameter yang diperlukan hilang (code dan state)",
"manual_url_auth_failed": "Autentikasi URL manual gagal",
"manual_url_auth_error": "Autentikasi gagal"
}
},
"warnings": {
"no_terminal_content": "Tidak ada konten terminal yang dipilih",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.",
"organization_mismatch": "Kamu harus diautentikasi dengan akun Roo Code Cloud organisasi kamu.",
"manual_url_empty": "Silakan masukkan URL callback yang valid",
"manual_url_no_query": "URL callback tidak valid: parameter query hilang",
"manual_url_missing_params": "URL callback tidak valid: parameter yang diperlukan hilang (code dan state)",
"manual_url_auth_failed": "Autentikasi URL manual gagal",
"manual_url_auth_error": "Autentikasi gagal",
"verification_failed": "Tidak dapat memverifikasi autentikasi organisasi."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Timeout durante il tentativo di ripristinare il checkpoint.",
"checkpoint_failed": "Impossibile ripristinare il checkpoint.",
"git_not_installed": "Git è richiesto per la funzione di checkpoint. Per favore, installa Git per abilitare i checkpoint.",
"nested_git_repos_warning": "I checkpoint sono disabilitati perché sono stati rilevati repository git annidati nell'area di lavoro. Per utilizzare i checkpoint, rimuovi o sposta i repository git annidati.",
"no_workspace": "Per favore, apri prima una cartella di progetto",
"update_support_prompt": "Errore durante l'aggiornamento del messaggio di supporto",
"reset_support_prompt": "Errore durante il ripristino del messaggio di supporto",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Nessuna attività attiva da cui eliminare messaggi",
"invalid_timestamp_for_deletion": "Timestamp del messaggio non valido per l'eliminazione",
"cannot_delete_missing_timestamp": "Impossibile eliminare il messaggio: timestamp mancante",
"cannot_delete_invalid_timestamp": "Impossibile eliminare il messaggio: timestamp non valido",
"message_not_found": "Messaggio con timestamp {{messageTs}} non trovato",
"error_deleting_message": "Errore durante l'eliminazione del messaggio: {{error}}",
"error_editing_message": "Errore durante la modifica del messaggio: {{error}}"
},
"gemini": {
"generate_stream": "Errore del flusso di contesto di generazione Gemini: {{error}}",
"generate_complete_prompt": "Errore di completamento Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "La chiave API contiene caratteri non validi."
},
"manual_url_empty": "Inserisci un URL di callback valido",
"manual_url_no_query": "URL di callback non valido: parametri di query mancanti",
"manual_url_missing_params": "URL di callback non valido: parametri richiesti mancanti (code e state)",
"manual_url_auth_failed": "Autenticazione manuale tramite URL fallita",
"manual_url_auth_error": "Autenticazione fallita"
}
},
"warnings": {
"no_terminal_content": "Nessun contenuto del terminale selezionato",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.",
"organization_mismatch": "Devi essere autenticato con l'account Roo Code Cloud della tua organizzazione.",
"manual_url_empty": "Inserisci un URL di callback valido",
"manual_url_no_query": "URL di callback non valido: parametri di query mancanti",
"manual_url_missing_params": "URL di callback non valido: parametri richiesti mancanti (code e state)",
"manual_url_auth_failed": "Autenticazione manuale tramite URL fallita",
"manual_url_auth_error": "Autenticazione fallita",
"verification_failed": "Impossibile verificare l'autenticazione dell'organizzazione."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。",
"checkpoint_failed": "チェックポイントの復元に失敗しました。",
"git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。",
"nested_git_repos_warning": "ワークスペースでネストされたgitリポジトリが検出されたため、チェックポイントが無効になっています。チェックポイントを使用するには、ネストされたgitリポジトリを削除または移動してください。",
"no_workspace": "まずプロジェクトフォルダを開いてください",
"update_support_prompt": "サポートメッセージの更新に失敗しました",
"reset_support_prompt": "サポートメッセージのリセットに失敗しました",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "メッセージを削除するアクティブなタスクがありません",
"invalid_timestamp_for_deletion": "削除用のメッセージタイムスタンプが無効です",
"cannot_delete_missing_timestamp": "メッセージを削除できません:タイムスタンプがありません",
"cannot_delete_invalid_timestamp": "メッセージを削除できません:タイムスタンプが無効です",
"message_not_found": "タイムスタンプ {{messageTs}} のメッセージが見つかりません",
"error_deleting_message": "メッセージ削除エラー:{{error}}",
"error_editing_message": "メッセージ編集エラー:{{error}}"
},
"gemini": {
"generate_stream": "Gemini 生成コンテキスト ストリーム エラー: {{error}}",
"generate_complete_prompt": "Gemini 完了エラー: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。"
},
"manual_url_empty": "有効なコールバック URL を入力してください",
"manual_url_no_query": "無効なコールバック URLクエリパラメータがありません",
"manual_url_missing_params": "無効なコールバック URL必要なパラメータcode と stateがありません",
"manual_url_auth_failed": "手動 URL 認証が失敗しました",
"manual_url_auth_error": "認証に失敗しました"
}
},
"warnings": {
"no_terminal_content": "選択されたターミナルコンテンツがありません",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。",
"organization_mismatch": "組織の Roo Code Cloud アカウントで認証する必要があります。",
"manual_url_empty": "有効なコールバック URL を入力してください",
"manual_url_no_query": "無効なコールバック URLクエリパラメータがありません",
"manual_url_missing_params": "無効なコールバック URL必要なパラメータcode と stateがありません",
"manual_url_auth_failed": "手動 URL 認証が失敗しました",
"manual_url_auth_error": "認証に失敗しました",
"verification_failed": "組織認証の確認ができませんでした。"
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.",
"checkpoint_failed": "체크포인트 복원에 실패했습니다.",
"git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.",
"nested_git_repos_warning": "워크스페이스에서 중첩된 git 저장소가 감지되어 체크포인트가 비활성화되었습니다. 체크포인트를 사용하려면 중첩된 git 저장소를 제거하거나 이동해주세요.",
"no_workspace": "먼저 프로젝트 폴더를 열어주세요",
"update_support_prompt": "지원 프롬프트 업데이트에 실패했습니다",
"reset_support_prompt": "지원 프롬프트 재설정에 실패했습니다",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "메시지를 삭제할 활성 작업이 없습니다",
"invalid_timestamp_for_deletion": "삭제를 위한 메시지 타임스탬프가 유효하지 않습니다",
"cannot_delete_missing_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 없습니다",
"cannot_delete_invalid_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 유효하지 않습니다",
"message_not_found": "타임스탬프 {{messageTs}}인 메시지를 찾을 수 없습니다",
"error_deleting_message": "메시지 삭제 오류: {{error}}",
"error_editing_message": "메시지 편집 오류: {{error}}"
},
"gemini": {
"generate_stream": "Gemini 생성 컨텍스트 스트림 오류: {{error}}",
"generate_complete_prompt": "Gemini 완료 오류: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다."
},
"manual_url_empty": "유효한 콜백 URL을 입력하세요",
"manual_url_no_query": "유효하지 않은 콜백 URL: 쿼리 매개변수 누락",
"manual_url_missing_params": "유효하지 않은 콜백 URL: 필요한 매개변수 누락 (code와 state)",
"manual_url_auth_failed": "수동 URL 인증 실패",
"manual_url_auth_error": "인증 실패"
}
},
"warnings": {
"no_terminal_content": "선택된 터미널 내용이 없습니다",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",
"organization_mismatch": "조직의 Roo Code Cloud 계정으로 인증해야 합니다.",
"manual_url_empty": "유효한 콜백 URL을 입력하세요",
"manual_url_no_query": "유효하지 않은 콜백 URL: 쿼리 매개변수 누락",
"manual_url_missing_params": "유효하지 않은 콜백 URL: 필요한 매개변수 누락 (code와 state)",
"manual_url_auth_failed": "수동 URL 인증 실패",
"manual_url_auth_error": "인증 실패",
"verification_failed": "조직 인증을 확인할 수 없습니다."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Time-out bij het herstellen van checkpoint.",
"checkpoint_failed": "Herstellen van checkpoint mislukt.",
"git_not_installed": "Git is vereist voor de checkpoint-functie. Installeer Git om checkpoints in te schakelen.",
"nested_git_repos_warning": "Checkpoints zijn uitgeschakeld omdat geneste git-repositories zijn gedetecteerd in de werkruimte. Om checkpoints te gebruiken, verwijder of verplaats de geneste git-repositories.",
"no_workspace": "Open eerst een projectmap",
"update_support_prompt": "Bijwerken van ondersteuningsprompt mislukt",
"reset_support_prompt": "Resetten van ondersteuningsprompt mislukt",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Geen actieve taak om berichten uit te verwijderen",
"invalid_timestamp_for_deletion": "Ongeldig bericht tijdstempel voor verwijdering",
"cannot_delete_missing_timestamp": "Kan bericht niet verwijderen: tijdstempel ontbreekt",
"cannot_delete_invalid_timestamp": "Kan bericht niet verwijderen: ongeldig tijdstempel",
"message_not_found": "Bericht met tijdstempel {{messageTs}} niet gevonden",
"error_deleting_message": "Fout bij verwijderen van bericht: {{error}}",
"error_editing_message": "Fout bij bewerken van bericht: {{error}}"
},
"gemini": {
"generate_stream": "Fout bij het genereren van contextstream door Gemini: {{error}}",
"generate_complete_prompt": "Fout bij het voltooien door Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API-sleutel bevat ongeldige karakters."
},
"manual_url_empty": "Voer een geldige callback-URL in",
"manual_url_no_query": "Ongeldige callback-URL: query-parameters ontbreken",
"manual_url_missing_params": "Ongeldige callback-URL: vereiste parameters ontbreken (code en state)",
"manual_url_auth_failed": "Handmatige URL-authenticatie mislukt",
"manual_url_auth_error": "Authenticatie mislukt"
}
},
"warnings": {
"no_terminal_content": "Geen terminalinhoud geselecteerd",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.",
"organization_mismatch": "Je moet geauthenticeerd zijn met het Roo Code Cloud-account van je organisatie.",
"manual_url_empty": "Voer een geldige callback-URL in",
"manual_url_no_query": "Ongeldige callback-URL: query-parameters ontbreken",
"manual_url_missing_params": "Ongeldige callback-URL: vereiste parameters ontbreken (code en state)",
"manual_url_auth_failed": "Handmatige URL-authenticatie mislukt",
"manual_url_auth_error": "Authenticatie mislukt",
"verification_failed": "Kan organisatie-authenticatie niet verifiëren."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Upłynął limit czasu podczas próby przywrócenia punktu kontrolnego.",
"checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.",
"git_not_installed": "Funkcja punktów kontrolnych wymaga oprogramowania Git. Zainstaluj Git, aby włączyć punkty kontrolne.",
"nested_git_repos_warning": "Punkty kontrolne są wyłączone, ponieważ wykryto zagnieżdżone repozytoria git w obszarze roboczym. Aby używać punktów kontrolnych, usuń lub przenieś zagnieżdżone repozytoria git.",
"no_workspace": "Najpierw otwórz folder projektu",
"update_support_prompt": "Nie udało się zaktualizować komunikatu wsparcia",
"reset_support_prompt": "Nie udało się zresetować komunikatu wsparcia",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Brak aktywnego zadania do usunięcia wiadomości",
"invalid_timestamp_for_deletion": "Nieprawidłowy znacznik czasu wiadomości do usunięcia",
"cannot_delete_missing_timestamp": "Nie można usunąć wiadomości: brak znacznika czasu",
"cannot_delete_invalid_timestamp": "Nie można usunąć wiadomości: nieprawidłowy znacznik czasu",
"message_not_found": "Wiadomość ze znacznikiem czasu {{messageTs}} nie została znaleziona",
"error_deleting_message": "Błąd usuwania wiadomości: {{error}}",
"error_editing_message": "Błąd edytowania wiadomości: {{error}}"
},
"gemini": {
"generate_stream": "Błąd strumienia kontekstu generowania Gemini: {{error}}",
"generate_complete_prompt": "Błąd uzupełniania Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "Klucz API zawiera nieprawidłowe znaki."
},
"manual_url_empty": "Wprowadź prawidłowy URL callback",
"manual_url_no_query": "Nieprawidłowy URL callback: brak parametrów zapytania",
"manual_url_missing_params": "Nieprawidłowy URL callback: brak wymaganych parametrów (code i state)",
"manual_url_auth_failed": "Ręczne uwierzytelnienie URL nie powiodło się",
"manual_url_auth_error": "Uwierzytelnienie nie powiodło się"
}
},
"warnings": {
"no_terminal_content": "Nie wybrano zawartości terminala",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.",
"organization_mismatch": "Musisz być uwierzytelniony kontem Roo Code Cloud swojej organizacji.",
"manual_url_empty": "Wprowadź prawidłowy URL callback",
"manual_url_no_query": "Nieprawidłowy URL callback: brak parametrów zapytania",
"manual_url_missing_params": "Nieprawidłowy URL callback: brak wymaganych parametrów (code i state)",
"manual_url_auth_failed": "Ręczne uwierzytelnienie URL nie powiodło się",
"manual_url_auth_error": "Uwierzytelnienie nie powiodło się",
"verification_failed": "Nie można zweryfikować uwierzytelnienia organizacji."
},
"info": {

View file

@ -33,7 +33,6 @@
"checkpoint_timeout": "Tempo esgotado ao tentar restaurar o ponto de verificação.",
"checkpoint_failed": "Falha ao restaurar o ponto de verificação.",
"git_not_installed": "O Git é necessário para o recurso de checkpoints. Por favor, instale o Git para habilitar os checkpoints.",
"nested_git_repos_warning": "Os checkpoints estão desabilitados porque repositórios git aninhados foram detectados no espaço de trabalho. Para usar checkpoints, por favor remova ou realoque os repositórios git aninhados.",
"no_workspace": "Por favor, abra primeiro uma pasta de projeto",
"update_support_prompt": "Falha ao atualizar o prompt de suporte",
"reset_support_prompt": "Falha ao redefinir o prompt de suporte",
@ -92,15 +91,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Nenhuma tarefa ativa para excluir mensagens",
"invalid_timestamp_for_deletion": "Timestamp da mensagem inválido para exclusão",
"cannot_delete_missing_timestamp": "Não é possível excluir mensagem: timestamp ausente",
"cannot_delete_invalid_timestamp": "Não é possível excluir mensagem: timestamp inválido",
"message_not_found": "Mensagem com timestamp {{messageTs}} não encontrada",
"error_deleting_message": "Erro ao excluir mensagem: {{error}}",
"error_editing_message": "Erro ao editar mensagem: {{error}}"
},
"gemini": {
"generate_stream": "Erro de fluxo de contexto de geração do Gemini: {{error}}",
"generate_complete_prompt": "Erro de conclusão do Gemini: {{error}}",
@ -120,12 +110,7 @@
},
"api": {
"invalidKeyInvalidChars": "A chave API contém caracteres inválidos."
},
"manual_url_empty": "Por favor, insira uma URL de callback válida",
"manual_url_no_query": "URL de callback inválida: parâmetros de consulta ausentes",
"manual_url_missing_params": "URL de callback inválida: parâmetros obrigatórios ausentes (code e state)",
"manual_url_auth_failed": "Autenticação manual por URL falhou",
"manual_url_auth_error": "Falha na autenticação"
}
},
"warnings": {
"no_terminal_content": "Nenhum conteúdo do terminal selecionado",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.",
"organization_mismatch": "Você deve estar autenticado com a conta Roo Code Cloud da sua organização.",
"manual_url_empty": "Por favor, insira uma URL de callback válida",
"manual_url_no_query": "URL de callback inválida: parâmetros de consulta ausentes",
"manual_url_missing_params": "URL de callback inválida: parâmetros obrigatórios ausentes (code e state)",
"manual_url_auth_failed": "Autenticação manual por URL falhou",
"manual_url_auth_error": "Falha na autenticação",
"verification_failed": "Não foi possível verificar a autenticação da organização."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.",
"checkpoint_failed": "Не удалось восстановить контрольную точку.",
"git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.",
"nested_git_repos_warning": "Контрольные точки отключены, поскольку в рабочем пространстве обнаружены вложенные git-репозитории. Чтобы использовать контрольные точки, пожалуйста, удалите или переместите вложенные git-репозитории.",
"no_workspace": "Пожалуйста, сначала откройте папку проекта",
"update_support_prompt": "Не удалось обновить промпт поддержки",
"reset_support_prompt": "Не удалось сбросить промпт поддержки",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Нет активной задачи для удаления сообщений",
"invalid_timestamp_for_deletion": "Недействительная временная метка сообщения для удаления",
"cannot_delete_missing_timestamp": "Невозможно удалить сообщение: отсутствует временная метка",
"cannot_delete_invalid_timestamp": "Невозможно удалить сообщение: недействительная временная метка",
"message_not_found": "Сообщение с временной меткой {{messageTs}} не найдено",
"error_deleting_message": "Ошибка удаления сообщения: {{error}}",
"error_editing_message": "Ошибка редактирования сообщения: {{error}}"
},
"gemini": {
"generate_stream": "Ошибка потока контекста генерации Gemini: {{error}}",
"generate_complete_prompt": "Ошибка завершения Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API-ключ содержит недопустимые символы."
},
"manual_url_empty": "Введи действительный URL обратного вызова",
"manual_url_no_query": "Недействительный URL обратного вызова: отсутствуют параметры запроса",
"manual_url_missing_params": "Недействительный URL обратного вызова: отсутствуют обязательные параметры (code и state)",
"manual_url_auth_failed": "Ручная аутентификация по URL не удалась",
"manual_url_auth_error": "Аутентификация не удалась"
}
},
"warnings": {
"no_terminal_content": "Не выбрано содержимое терминала",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.",
"organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Roo Code Cloud вашей организации.",
"manual_url_empty": "Введи действительный URL обратного вызова",
"manual_url_no_query": "Недействительный URL обратного вызова: отсутствуют параметры запроса",
"manual_url_missing_params": "Недействительный URL обратного вызова: отсутствуют обязательные параметры (code и state)",
"manual_url_auth_failed": "Ручная аутентификация по URL не удалась",
"manual_url_auth_error": "Аутентификация не удалась",
"verification_failed": "Не удается проверить аутентификацию организации."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Kontrol noktasını geri yüklemeye çalışırken zaman aşımına uğradı.",
"checkpoint_failed": "Kontrol noktası geri yüklenemedi.",
"git_not_installed": "Kontrol noktaları özelliği için Git gereklidir. Kontrol noktalarını etkinleştirmek için lütfen Git'i yükleyin.",
"nested_git_repos_warning": "Çalışma alanında iç içe git depoları tespit edildiği için kontrol noktaları devre dışı bırakıldı. Kontrol noktalarını kullanmak için lütfen iç içe git depolarını kaldırın veya taşıyın.",
"no_workspace": "Lütfen önce bir proje klasörü açın",
"update_support_prompt": "Destek istemi güncellenemedi",
"reset_support_prompt": "Destek istemi sıfırlanamadı",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Mesaj silinecek aktif görev yok",
"invalid_timestamp_for_deletion": "Silme için geçersiz mesaj zaman damgası",
"cannot_delete_missing_timestamp": "Mesaj silinemiyor: zaman damgası eksik",
"cannot_delete_invalid_timestamp": "Mesaj silinemiyor: geçersiz zaman damgası",
"message_not_found": "{{messageTs}} zaman damgalı mesaj bulunamadı",
"error_deleting_message": "Mesaj silme hatası: {{error}}",
"error_editing_message": "Mesaj düzenleme hatası: {{error}}"
},
"gemini": {
"generate_stream": "Gemini oluşturma bağlam akışı hatası: {{error}}",
"generate_complete_prompt": "Gemini tamamlama hatası: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "API anahtarı geçersiz karakterler içeriyor."
},
"manual_url_empty": "Lütfen geçerli bir callback URL'si girin",
"manual_url_no_query": "Geçersiz callback URL'si: sorgu parametreleri eksik",
"manual_url_missing_params": "Geçersiz callback URL'si: gerekli parametreler eksik (code ve state)",
"manual_url_auth_failed": "Manuel URL kimlik doğrulama başarısız",
"manual_url_auth_error": "Kimlik doğrulama başarısız"
}
},
"warnings": {
"no_terminal_content": "Seçili terminal içeriği yok",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.",
"organization_mismatch": "Kuruluşunuzun Roo Code Cloud hesabıyla kimlik doğrulaması yapmalısınız.",
"manual_url_empty": "Lütfen geçerli bir callback URL'si girin",
"manual_url_no_query": "Geçersiz callback URL'si: sorgu parametreleri eksik",
"manual_url_missing_params": "Geçersiz callback URL'si: gerekli parametreler eksik (code ve state)",
"manual_url_auth_failed": "Manuel URL kimlik doğrulama başarısız",
"manual_url_auth_error": "Kimlik doğrulama başarısız",
"verification_failed": "Kuruluş kimlik doğrulaması doğrulanamıyor."
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "Đã hết thời gian khi cố gắng khôi phục điểm kiểm tra.",
"checkpoint_failed": "Không thể khôi phục điểm kiểm tra.",
"git_not_installed": "Yêu cầu Git cho tính năng điểm kiểm tra. Vui lòng cài đặt Git để bật điểm kiểm tra.",
"nested_git_repos_warning": "Điểm kiểm tra bị vô hiệu hóa vì phát hiện các kho git lồng nhau trong không gian làm việc. Để sử dụng điểm kiểm tra, vui lòng xóa hoặc di chuyển các kho git lồng nhau.",
"no_workspace": "Vui lòng mở thư mục dự án trước",
"update_support_prompt": "Không thể cập nhật lời nhắc hỗ trợ",
"reset_support_prompt": "Không thể đặt lại lời nhắc hỗ trợ",
@ -88,15 +87,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "Không có nhiệm vụ hoạt động để xóa tin nhắn",
"invalid_timestamp_for_deletion": "Dấu thời gian tin nhắn không hợp lệ để xóa",
"cannot_delete_missing_timestamp": "Không thể xóa tin nhắn: thiếu dấu thời gian",
"cannot_delete_invalid_timestamp": "Không thể xóa tin nhắn: dấu thời gian không hợp lệ",
"message_not_found": "Không tìm thấy tin nhắn có dấu thời gian {{messageTs}}",
"error_deleting_message": "Lỗi xóa tin nhắn: {{error}}",
"error_editing_message": "Lỗi chỉnh sửa tin nhắn: {{error}}"
},
"gemini": {
"generate_stream": "Lỗi luồng ngữ cảnh tạo Gemini: {{error}}",
"generate_complete_prompt": "Lỗi hoàn thành Gemini: {{error}}",
@ -116,12 +106,7 @@
},
"api": {
"invalidKeyInvalidChars": "Khóa API chứa ký tự không hợp lệ."
},
"manual_url_empty": "Vui lòng nhập URL callback hợp lệ",
"manual_url_no_query": "URL callback không hợp lệ: thiếu tham số truy vấn",
"manual_url_missing_params": "URL callback không hợp lệ: thiếu tham số bắt buộc (code và state)",
"manual_url_auth_failed": "Xác thực URL thủ công thất bại",
"manual_url_auth_error": "Xác thực thất bại"
}
},
"warnings": {
"no_terminal_content": "Không có nội dung terminal được chọn",
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.",
"organization_mismatch": "Bạn phải được xác thực bằng tài khoản Roo Code Cloud của tổ chức.",
"manual_url_empty": "Vui lòng nhập URL callback hợp lệ",
"manual_url_no_query": "URL callback không hợp lệ: thiếu tham số truy vấn",
"manual_url_missing_params": "URL callback không hợp lệ: thiếu tham số bắt buộc (code và state)",
"manual_url_auth_failed": "Xác thực URL thủ công thất bại",
"manual_url_auth_error": "Xác thực thất bại",
"verification_failed": "Không thể xác minh xác thực tổ chức."
},
"info": {

View file

@ -34,7 +34,6 @@
"checkpoint_timeout": "尝试恢复检查点时超时。",
"checkpoint_failed": "恢复检查点失败。",
"git_not_installed": "存档点功能需要 Git。请安装 Git 以启用存档点。",
"nested_git_repos_warning": "存档点已禁用,因为在工作区中检测到嵌套的 git 仓库。要使用存档点,请移除或重新定位嵌套的 git 仓库。",
"no_workspace": "请先打开项目文件夹",
"update_support_prompt": "更新支持消息失败",
"reset_support_prompt": "重置支持消息失败",
@ -93,15 +92,6 @@
"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.",
"notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}"
},
"message": {
"no_active_task_to_delete": "没有可删除消息的活跃任务",
"invalid_timestamp_for_deletion": "删除操作的消息时间戳无效",
"cannot_delete_missing_timestamp": "无法删除消息:缺少时间戳",
"cannot_delete_invalid_timestamp": "无法删除消息:时间戳无效",
"message_not_found": "未找到时间戳为 {{messageTs}} 的消息",
"error_deleting_message": "删除消息时出错:{{error}}",
"error_editing_message": "编辑消息时出错:{{error}}"
},
"gemini": {
"generate_stream": "Gemini 生成上下文流错误:{{error}}",
"generate_complete_prompt": "Gemini 完成错误:{{error}}",
@ -121,12 +111,7 @@
},
"api": {
"invalidKeyInvalidChars": "API 密钥包含无效字符。"
},
"manual_url_empty": "请输入有效的回调 URL",
"manual_url_no_query": "无效的回调 URL缺少查询参数",
"manual_url_missing_params": "无效的回调 URL缺少必需参数code 和 state",
"manual_url_auth_failed": "手动 URL 身份验证失败",
"manual_url_auth_error": "身份验证失败"
}
},
"warnings": {
"no_terminal_content": "没有选择终端内容",
@ -213,11 +198,6 @@
"errors": {
"cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。",
"organization_mismatch": "您必须使用组织的 Roo Code Cloud 账户进行身份验证。",
"manual_url_empty": "请输入有效的回调 URL",
"manual_url_no_query": "无效的回调 URL缺少查询参数",
"manual_url_missing_params": "无效的回调 URL缺少必需参数code 和 state",
"manual_url_auth_failed": "手动 URL 身份验证失败",
"manual_url_auth_error": "身份验证失败",
"verification_failed": "无法验证组织身份验证。"
},
"info": {

View file

@ -29,7 +29,6 @@
"checkpoint_timeout": "嘗試恢復檢查點時超時。",
"checkpoint_failed": "恢復檢查點失敗。",
"git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。",
"nested_git_repos_warning": "存檔點已停用,因為在工作區中偵測到巢狀的 git 儲存庫。要使用存檔點,請移除或重新配置巢狀的 git 儲存庫。",
"no_workspace": "請先開啟專案資料夾",
"update_support_prompt": "更新支援訊息失敗",
"reset_support_prompt": "重設支援訊息失敗",
@ -87,15 +86,6 @@
"apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。",
"notFound": "找不到 Claude Code 可執行檔案 '{{claudePath}}'。\n\n請安裝 Claude Code CLI\n1. 造訪 {{installationUrl}} 下載 Claude Code\n2. 依照作業系統的安裝說明進行操作\n3. 確保 'claude' 指令在 PATH 中可用\n4. 或者在 Roo 設定中的 'Claude Code 路徑' 下設定自訂路徑\n\n原始錯誤{{originalError}}"
},
"message": {
"no_active_task_to_delete": "沒有可刪除訊息的活躍工作",
"invalid_timestamp_for_deletion": "刪除操作的訊息時間戳無效",
"cannot_delete_missing_timestamp": "無法刪除訊息:缺少時間戳",
"cannot_delete_invalid_timestamp": "無法刪除訊息:時間戳無效",
"message_not_found": "未找到時間戳為 {{messageTs}} 的訊息",
"error_deleting_message": "刪除訊息時出錯:{{error}}",
"error_editing_message": "編輯訊息時出錯:{{error}}"
},
"gemini": {
"generate_stream": "Gemini 產生內容串流錯誤:{{error}}",
"generate_complete_prompt": "Gemini 完成錯誤:{{error}}",
@ -116,11 +106,6 @@
"api": {
"invalidKeyInvalidChars": "API 金鑰包含無效字元。"
},
"manual_url_empty": "請輸入有效的回呼 URL",
"manual_url_no_query": "無效的回呼 URL缺少查詢參數",
"manual_url_missing_params": "無效的回呼 URL缺少必要參數code 和 state",
"manual_url_auth_failed": "手動 URL 身份驗證失敗",
"manual_url_auth_error": "身份驗證失敗",
"mode_import_failed": "匯入模式失敗:{{error}}"
},
"warnings": {
@ -208,11 +193,6 @@
"errors": {
"cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。",
"organization_mismatch": "您必須使用組織的 Roo Code Cloud 帳戶進行身份驗證。",
"manual_url_empty": "請輸入有效的回呼 URL",
"manual_url_no_query": "無效的回呼 URL缺少查詢參數",
"manual_url_missing_params": "無效的回呼 URL缺少必要參數code 和 state",
"manual_url_auth_failed": "手動 URL 身份驗證失敗",
"manual_url_auth_error": "身份驗證失敗",
"verification_failed": "無法驗證組織身份驗證。"
},
"info": {

View file

@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.28.1",
"version": "3.27.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",

View file

@ -6,11 +6,9 @@ import EventEmitter from "events"
import simpleGit, { SimpleGit } from "simple-git"
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import { fileExistsAtPath } from "../../utils/fs"
import { executeRipgrep } from "../../services/search/file-search"
import { t } from "../../i18n"
import { CheckpointDiff, CheckpointResult, CheckpointEventMap } from "./types"
import { getExcludePatterns } from "./excludes"
@ -73,9 +71,6 @@ export abstract class ShadowCheckpointService extends EventEmitter {
const hasNestedGitRepos = await this.hasNestedGitRepositories()
if (hasNestedGitRepos) {
// Show user-friendly notification
vscode.window.showWarningMessage(t("common:errors.nested_git_repos_warning"), "OK")
throw new Error(
"Checkpoints are disabled because nested git repositories were detected in the workspace. " +
"Please remove or relocate nested git repositories to use the checkpoints feature.",

View file

@ -123,7 +123,6 @@ export interface ExtensionMessage {
| "showEditMessageDialog"
| "commands"
| "insertTextIntoTextarea"
| "dismissedUpsells"
text?: string
payload?: any // Add a generic payload for now, can refine later
action?:
@ -149,7 +148,7 @@ export interface ExtensionMessage {
clineMessage?: ClineMessage
routerModels?: RouterModels
openAiModels?: string[]
ollamaModels?: ModelRecord
ollamaModels?: string[]
lmStudioModels?: ModelRecord
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
huggingFaceModels?: Array<{
@ -200,7 +199,6 @@ export interface ExtensionMessage {
context?: string
commands?: Command[]
queuedMessages?: QueuedMessage[]
list?: string[] // For dismissedUpsells
}
export type ExtensionState = Pick<
@ -211,7 +209,6 @@ export type ExtensionState = Pick<
// | "lastShownAnnouncementId"
| "customInstructions"
// | "taskHistory" // Optional in GlobalSettings, required here.
| "dismissedUpsells"
| "autoApprovalEnabled"
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
@ -281,6 +278,7 @@ export type ExtensionState = Pick<
| "profileThresholds"
| "includeDiagnosticMessages"
| "maxDiagnosticMessages"
| "remoteControlEnabled"
| "openRouterImageGenerationSelectedModel"
| "includeTaskHistoryInEnhance"
> & {
@ -344,9 +342,6 @@ export type ExtensionState = Pick<
mcpServers?: McpServer[]
hasSystemPromptOverride?: boolean
mdmCompliant?: boolean
remoteControlEnabled: boolean
taskSyncEnabled: boolean
featureRoomoteControlEnabled: boolean
}
export interface ClineSayTool {

View file

@ -134,7 +134,6 @@ export interface WebviewMessage {
| "mcpEnabled"
| "enableMcpServerCreation"
| "remoteControlEnabled"
| "taskSyncEnabled"
| "searchCommits"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
@ -181,7 +180,6 @@ export interface WebviewMessage {
| "cloudButtonClicked"
| "rooCloudSignIn"
| "rooCloudSignOut"
| "rooCloudManualUrl"
| "condenseTaskContextRequest"
| "requestIndexingStatus"
| "startIndexing"
@ -223,8 +221,6 @@ export interface WebviewMessage {
| "queueMessage"
| "removeQueuedMessage"
| "editQueuedMessage"
| "dismissUpsell"
| "getDismissedUpsells"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@ -270,8 +266,6 @@ export interface WebviewMessage {
visibility?: ShareVisibility // For share visibility
hasContent?: boolean // For checkRulesDirectoryResult
checkOnly?: boolean // For deleteCustomMode check
upsellId?: string // For dismissUpsell
list?: string[] // For dismissedUpsells response
codeIndexSettings?: {
// Global state settings
codebaseIndexEnabled: boolean

View file

@ -3,55 +3,37 @@ import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
describe("mentionRegex and mentionRegexGlobal", () => {
// Test cases for various mention types
const testCases = [
// Basic file paths at line start
// Basic file paths
{ input: "@/path/to/file.txt", expected: ["@/path/to/file.txt"] },
{ input: "@/file.js", expected: ["@/file.js"] },
{ input: "@/folder/", expected: ["@/folder/"] },
// File paths with escaped spaces at line start
// File paths with escaped spaces
{ input: "@/path/to/file\\ with\\ spaces.txt", expected: ["@/path/to/file\\ with\\ spaces.txt"] },
{ input: "@/users/my\\ project/report\\ final.pdf", expected: ["@/users/my\\ project/report\\ final.pdf"] },
{ input: "@/folder\\ with\\ spaces/", expected: ["@/folder\\ with\\ spaces/"] },
{ input: "@/a\\ b\\ c.txt", expected: ["@/a\\ b\\ c.txt"] },
// URLs at line start
// URLs
{ input: "@http://example.com", expected: ["@http://example.com"] },
{ input: "@https://example.com/path?query=1", expected: ["@https://example.com/path?query=1"] },
// Other mentions at line start
// Other mentions
{ input: "@problems", expected: ["@problems"] },
{ input: "@git-changes", expected: ["@git-changes"] },
{ input: "@terminal", expected: ["@terminal"] },
{ input: "@a1b2c3d", expected: ["@a1b2c3d"] }, // Git commit hash (short)
{ input: "@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", expected: ["@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"] }, // Git commit hash (long)
// Mentions after whitespace (valid)
// Mentions within text
{
input: "Check file @/path/to/file\\ with\\ spaces.txt for details.",
expected: ["@/path/to/file\\ with\\ spaces.txt"],
},
{ input: "See @problems and @terminal output.", expected: ["@problems", "@terminal"] },
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // After colon and space
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // Trailing punctuation
{ input: "Commit @a1b2c3d, then check @/file.txt", expected: ["@a1b2c3d", "@/file.txt"] },
// NEW: Test cases for mentions mid-line without whitespace (should NOT match)
{ input: "error@https://example.com/path", expected: null }, // @ mid-word before URL
{ input: "log:@/var/log/system.log", expected: null }, // @ after colon without space
{ input: "email@problems", expected: null }, // @ mid-word before "problems"
{ input: "path=@/usr/local/bin", expected: null }, // @ after equals without space
{ input: "Error at line 42@terminal output", expected: null }, // @ mid-line before "terminal"
{ input: "commit@a1b2c3d", expected: null }, // @ mid-word before git hash
// NEW: Test cases for pasted logs (should NOT match)
{ input: "Failed to fetch@https://api.example.com/endpoint", expected: null },
{ input: "Error loading resource@/assets/image.png", expected: null },
{ input: "Stack trace:@/home/user/project/file.js:42", expected: null },
// NEW: Valid mentions with various whitespace
{ input: "Check\t@/path/to/file.txt", expected: ["@/path/to/file.txt"] }, // Tab before @
{ input: "Multiple @problems here", expected: ["@problems"] }, // Multiple spaces
{ input: "Newline\n@terminal output", expected: ["@terminal"] }, // After newline
// Negative cases (should not match or match partially)
{ input: "@/path/with unescaped space.txt", expected: ["@/path/with"] }, // Unescaped space
{ input: "@ /path/leading-space.txt", expected: null }, // Space after @
@ -59,15 +41,14 @@ describe("mentionRegex and mentionRegexGlobal", () => {
{ input: "mention@", expected: null }, // Trailing @
{ input: "@/path/trailing\\", expected: null }, // Trailing backslash (invalid escape)
{ input: "@/path/to/file\\not-a-space", expected: null }, // Backslash not followed by space
// Escaped mentions (should not match due to negative lookbehind)
{ input: "This is not a mention: \\@/path/to/file.txt", expected: null },
{ input: "Escaped \\@problems word", expected: null },
{ input: "Text with \\@https://example.com", expected: null },
{ input: "Another \\@a1b2c3d hash", expected: null },
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // After space, should work
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // Ensure non-escaped still works nearby
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash, currently incorrectly fails to match
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces within the path part
]
testCases.forEach(({ input, expected }) => {
it(`should handle input: "${input}"`, () => {
@ -102,48 +83,4 @@ describe("mentionRegex and mentionRegexGlobal", () => {
expect(matches[0][1]).toBe("/path/to/escaped\\ file.txt") // Group 1 should not include '@'
expect(matches[1][1]).toBe("problems")
})
// NEW: Additional tests for the boundary restriction
describe("boundary restrictions", () => {
it("should match mentions at the start of a line", () => {
const input = "@/path/to/file.txt"
const match = input.match(mentionRegex)
expect(match).not.toBeNull()
expect(match?.[0]).toBe("@/path/to/file.txt")
})
it("should match mentions after whitespace", () => {
const input = "Check @/path/to/file.txt"
const match = input.match(mentionRegex)
expect(match).not.toBeNull()
expect(match?.[0]).toBe("@/path/to/file.txt")
})
it("should NOT match mentions mid-word or after non-whitespace", () => {
const testCases = ["error@https://example.com", "path:@/var/log", "email@problems", "42@terminal"]
testCases.forEach((input) => {
const match = input.match(mentionRegex)
expect(match).toBeNull()
})
})
it("should handle multiline text correctly", () => {
const input = `First line
@/path/on/newline.txt
Mid-line@/should/not/match.txt
After space @/should/match.txt`
const matches = Array.from(input.matchAll(mentionRegexGlobal))
expect(matches.length).toBe(2)
expect(matches[0][0]).toBe("@/path/on/newline.txt")
expect(matches[1][0]).toBe("@/should/match.txt")
})
it("should not match mentions in pasted log entries", () => {
const logEntry = "Error: Failed to load resource@https://api.example.com/data status:404"
const matches = Array.from(logEntry.matchAll(mentionRegexGlobal))
expect(matches.length).toBe(0)
})
})
})

View file

@ -1,25 +1,19 @@
/*
Mention regex:
- **Purpose**:
- To identify and highlight specific mentions in text that start with '@'.
- **Purpose**:
- To identify and highlight specific mentions in text that start with '@'.
- These mentions can be file paths, URLs, or the exact word 'problems'.
- Ensures that trailing punctuation marks (like commas, periods, etc.) are not included in the match, allowing punctuation to follow the mention without being part of it.
- Restricts @ parsing to line-start or after whitespace to avoid accidental loading from pasted logs.
- **Regex Breakdown**:
- `(?:^|\s)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them.
- **Line Start or Whitespace (`^|\s`)**: The @ must be at the start of a line or preceded by whitespace.
- `(?<!\\)@`:
- **Negative Lookbehind (`(?<!\\)`)**: Ensures the @ is not escaped with a backslash.
- `/@`:
- **@**: The mention must start with the '@' symbol.
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b)`:
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
- `(?:\/|\w+:\/\/)`:
- `(?:\/|\w+:\/\/)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
- `\/`:
- `\/`:
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
- `|`: Logical OR.
- `\w+:\/\/`:
@ -31,7 +25,7 @@ Mention regex:
- **Escaped Space (`\\ `)**: Matches a backslash followed by a space (an escaped space).
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
- `|`: Logical OR.
- `problems\b`:
- `problems\b`:
- **Exact Word ('problems')**: Matches the exact word 'problems'.
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
- `|`: Logical OR.
@ -40,9 +34,9 @@ Mention regex:
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
- `[.,;:!?]?`:
- `[.,;:!?]?`:
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
- `(?=[\s\r\n]|$)`:
- `(?=[\s\r\n]|$)`:
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
- **Summary**:
@ -54,14 +48,13 @@ Mention regex:
- The exact word 'git-changes'.
- The exact word 'terminal'.
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
- **NEW**: The @ symbol must be at the start of a line or preceded by whitespace to prevent accidental matches in pasted logs.
- **Global Regex**:
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
*/
export const mentionRegex =
/(?:^|(?<=\s))(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
/(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
// Regex to match command mentions like /command-name anywhere in text

View file

@ -32,7 +32,6 @@
"@roo-code/types": "workspace:^",
"@tailwindcss/vite": "^4.0.0",
"@tanstack/react-query": "^5.68.0",
"@types/qrcode": "^1.5.5",
"@vscode/codicons": "^0.0.36",
"@vscode/webview-ui-toolkit": "^1.4.0",
"axios": "^1.7.4",
@ -53,7 +52,6 @@
"mermaid": "^11.4.1",
"posthog-js": "^1.227.2",
"pretty-bytes": "^7.0.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.1",

View file

@ -192,11 +192,11 @@ const App = () => {
useEvent("message", onMessage)
useEffect(() => {
if (shouldShowAnnouncement && tab === "chat") {
if (shouldShowAnnouncement) {
setShowAnnouncement(true)
vscode.postMessage({ type: "didShowAnnouncement" })
}
}, [shouldShowAnnouncement, tab])
}, [shouldShowAnnouncement])
useEffect(() => {
if (didHydrateState) {

View file

@ -6,12 +6,9 @@ import { Package } from "@roo/package"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@src/components/ui"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@src/components/ui"
import { Button } from "@src/components/ui"
// Define the production URL constant locally to avoid importing from cloud package in webview
const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
interface AnnouncementProps {
hideAnnouncement: () => void
}
@ -28,8 +25,7 @@ interface AnnouncementProps {
const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
const { t } = useAppTranslation()
const [open, setOpen] = useState(true)
const { cloudApiUrl } = useExtensionState()
const cloudUrl = cloudApiUrl || PRODUCTION_ROO_CODE_API_URL
const { cloudIsAuthenticated } = useExtensionState()
return (
<Dialog
@ -44,82 +40,95 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
<DialogContent className="max-w-96">
<DialogHeader>
<DialogTitle>{t("chat:announcement.title", { version: Package.version })}</DialogTitle>
<DialogDescription>
<Trans
i18nKey="chat:announcement.description"
components={{
bold: <b />,
}}
/>
</DialogDescription>
</DialogHeader>
<div>
<ul className="space-y-2">
<li>
{" "}
<div className="space-y-2">
<div>
<Trans
i18nKey="chat:announcement.feature1"
i18nKey="chat:announcement.stealthModel.feature"
components={{
bold: <b />,
code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
}}
/>
</li>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature2"
components={{
bold: <b />,
}}
/>
</li>
</ul>
</div>
</div>
<div className="mt-4">
<Trans
i18nKey="chat:announcement.learnMore"
i18nKey="chat:announcement.stealthModel.note"
components={{
learnMoreLink: (
<VSCodeLink
href="https://docs.roocode.com/update-notes/v3.28.0#task-sync--roomote-control"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{
type: "action",
action: "openExternal",
data: {
url: "https://docs.roocode.com/update-notes/v3.28.0#task-sync--roomote-control",
},
},
"*",
)
bold: <b />,
code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
}}
/>
</div>
<div className="mt-4">
{!cloudIsAuthenticated ? (
<div className="space-y-3">
<div className="text-sm w-full">
<Trans
i18nKey="chat:announcement.stealthModel.selectModel"
components={{
code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
settingsLink: (
<VSCodeLink
href="#"
onClick={(e) => {
e.preventDefault()
setOpen(false)
hideAnnouncement()
window.postMessage(
{
type: "action",
action: "settingsButtonClicked",
values: { section: "provider" },
},
"*",
)
}}
/>
),
}}
/>
),
}}
/>
</div>
<div className="mt-4">
<Button
onClick={() => {
vscode.postMessage({ type: "openExternal", url: cloudUrl })
}}
className="w-full">
{t("chat:announcement.visitCloudButton")}
</Button>
</div>
<div className="mt-4 text-sm text-center">
<Trans
i18nKey="chat:announcement.socialLinks"
components={{
xLink: <XLink />,
discordLink: <DiscordLink />,
redditLink: <RedditLink />,
}}
/>
</div>
<Button
onClick={() => {
vscode.postMessage({ type: "rooCloudSignIn" })
}}
className="w-full">
{t("chat:announcement.stealthModel.connectButton")}
</Button>
</div>
) : (
<div className="text-sm w-full">
<Trans
i18nKey="chat:announcement.stealthModel.selectModel"
components={{
code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
settingsLink: (
<VSCodeLink
href="#"
onClick={(e) => {
e.preventDefault()
setOpen(false)
hideAnnouncement()
window.postMessage(
{
type: "action",
action: "settingsButtonClicked",
values: { section: "provider" },
},
"*",
)
}}
/>
),
}}
/>
</div>
)}
</div>
</div>
</DialogContent>
@ -127,43 +136,4 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
)
}
const XLink = () => (
<VSCodeLink
href="https://x.com/roo_code"
onClick={(e) => {
e.preventDefault()
window.postMessage({ type: "action", action: "openExternal", data: { url: "https://x.com/roo_code" } }, "*")
}}>
X
</VSCodeLink>
)
const DiscordLink = () => (
<VSCodeLink
href="https://discord.gg/rCQcvT7Fnt"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://discord.gg/rCQcvT7Fnt" } },
"*",
)
}}>
Discord
</VSCodeLink>
)
const RedditLink = () => (
<VSCodeLink
href="https://www.reddit.com/r/RooCode/"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://www.reddit.com/r/RooCode/" } },
"*",
)
}}>
r/RooCode
</VSCodeLink>
)
export default memo(Announcement)

View file

@ -9,9 +9,6 @@ import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from
import { StandardTooltip } from "@src/components/ui"
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
interface AutoApproveMenuProps {
style?: React.CSSProperties
@ -38,12 +35,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { t } = useAppTranslation()
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
autoOpenOnAuth: false,
})
const baseToggles = useAutoApprovalToggles()
const enabledCount = useMemo(() => Object.values(baseToggles).filter(Boolean).length, [baseToggles])
// AutoApproveMenu needs alwaysApproveResubmit in addition to the base toggles
const toggles = useMemo(
@ -181,23 +173,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
</div>
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
{enabledCount > 7 && (
<>
<DismissibleUpsell
upsellId="autoApprovePowerUserA"
onClick={() => openUpsell()}
dismissOnClick={false}
variant="banner">
<Trans
i18nKey="cloud:upsell.autoApprovePowerUser"
components={{
learnMoreLink: <VSCodeLink href="#" />,
}}
/>
</DismissibleUpsell>
</>
)}
</div>
)}
@ -265,7 +240,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
/>
</div>
</div>
<CloudUpsellDialog open={isOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
</div>
)
}

View file

@ -17,7 +17,8 @@ import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
import { vscode } from "@src/utils/vscode"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { Button } from "@src/components/ui"
import { formatTokenStats } from "@src/utils/formatTokens"
import { Button, StandardTooltip } from "@src/components/ui"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
@ -118,6 +119,7 @@ export const ChatRowContent = ({
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
@ -179,13 +181,20 @@ export const ChatRowContent = ({
vscode.postMessage({ type: "selectImages", context: "edit", messageTs: message.ts })
}, [message.ts])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, tokensIn, tokensOut, cacheReads] = useMemo(() => {
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
const info = safeJsonParse<ClineApiReqInfo>(message.text)
return [info?.cost, info?.cancelReason, info?.streamingFailedMessage]
return [
info?.cost,
info?.cancelReason,
info?.streamingFailedMessage,
info?.tokensIn,
info?.tokensOut,
info?.cacheReads,
]
}
return [undefined, undefined, undefined]
return [undefined, undefined, undefined, undefined, undefined, undefined]
}, [message.text, message.say])
// When resuming task, last wont be api_req_failed but a resume_task
@ -1086,13 +1095,15 @@ export const ChatRowContent = ({
return (
<ReasoningBlock
content={message.text || ""}
ts={message.ts}
isStreaming={isStreaming}
isLast={isLast}
metadata={message.metadata as any}
elapsed={isLast && isStreaming ? Date.now() - message.ts : undefined}
isCollapsed={reasoningCollapsed}
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
/>
)
case "api_req_started":
const tokenStats = formatTokenStats(tokensIn, tokensOut, cacheReads)
const hasTokenData = tokensIn !== undefined || tokensOut !== undefined
return (
<>
<div
@ -1111,13 +1122,97 @@ export const ChatRowContent = ({
msUserSelect: "none",
}}
onClick={handleToggleExpand}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
minWidth: 0,
}}>
{icon}
{title}
<VSCodeBadge
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
{hasTokenData ? (
<StandardTooltip
content={
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span> {t("chat:apiRequest.input")}:</span>
<span className="font-mono">{tokenStats.input}</span>
</div>
<div className="flex items-center gap-2">
<span> {t("chat:apiRequest.output")}:</span>
<span className="font-mono">{tokenStats.output}</span>
</div>
</div>
}
side="top">
<span
className="api-request-text"
style={{
display: "inline-block",
fontWeight: "bold",
color: "var(--vscode-foreground)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
flexShrink: 1,
minWidth: 0,
cursor: "default",
}}>
{title}
</span>
</StandardTooltip>
) : (
<span
className="api-request-text"
style={{
display: "inline-block",
fontWeight: "bold",
color: "var(--vscode-foreground)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
flexShrink: 1,
minWidth: 0,
}}>
{title}
</span>
)}
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }}>
{hasTokenData && cost !== null && cost !== undefined && cost > 0 ? (
<StandardTooltip
content={
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span> {t("chat:apiRequest.input")}:</span>
<span className="font-mono">{tokenStats.input}</span>
</div>
<div className="flex items-center gap-2">
<span> {t("chat:apiRequest.output")}:</span>
<span className="font-mono">{tokenStats.output}</span>
</div>
</div>
}
side="top">
<VSCodeBadge
style={{
opacity: 1,
flexShrink: 0,
cursor: "default",
}}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
</StandardTooltip>
) : (
<VSCodeBadge
style={{
opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0,
flexShrink: 0,
}}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
)}
</div>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
@ -1172,10 +1267,9 @@ export const ChatRowContent = ({
)
case "user_feedback":
return (
<div
className={`bg-vscode-editor-background border rounded-xs overflow-hidden whitespace-pre-wrap ${isEditing ? "p-0" : "p-1"}`}>
<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
{isEditing ? (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 p-2">
<ChatTextArea
inputValue={editedContent}
setInputValue={setEditedContent}
@ -1196,15 +1290,7 @@ export const ChatRowContent = ({
</div>
) : (
<div className="flex justify-between">
<div
className="flex-grow px-2 py-1 wrap-anywhere cursor-pointer hover:bg-vscode-list-hoverBackground rounded transition-colors"
onClick={(e) => {
e.stopPropagation()
if (!isStreaming) {
handleEditClick()
}
}}
title={t("chat:queuedMessages.clickToEdit")}>
<div className="flex-grow px-2 py-1 wrap-anywhere">
<Mention text={message.text} withShadow />
</div>
<div className="flex">

View file

@ -903,8 +903,20 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return (
<div
className={cn(
"relative flex flex-col gap-1 bg-editor-background outline-none border border-none box-border",
isEditMode ? "p-2 w-full" : "px-1.5 pb-1 w-[calc(100%-16px)] ml-auto mr-auto",
"relative",
"flex",
"flex-col",
"gap-1",
"bg-editor-background",
"px-1.5",
"pb-1",
"outline-none",
"border",
"border-none",
"w-[calc(100%-16px)]",
"ml-auto",
"mr-auto",
"box-border",
)}>
<div className="relative">
<div
@ -993,12 +1005,11 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
: isDraggingOver
? "border-2 border-dashed border-vscode-focusBorder"
: "border border-transparent",
"pl-2",
"py-2",
isEditMode ? "pr-20" : "pr-9",
"px-[8px]",
"py-1.5",
"pr-9",
"z-10",
"forced-color-adjust-none",
"rounded",
)}
style={{
color: "transparent",
@ -1019,15 +1030,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
updateHighlights()
}}
onFocus={() => setIsFocused(true)}
onKeyDown={(e) => {
// Handle ESC to cancel in edit mode
if (isEditMode && e.key === "Escape" && !e.nativeEvent?.isComposing) {
e.preventDefault()
onCancel?.()
return
}
handleKeyDown(e)
}}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onBlur={handleBlur}
onPaste={handlePaste}
@ -1051,7 +1054,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"text-vscode-editor-font-size",
"leading-vscode-editor-line-height",
"cursor-text",
"py-2 pl-2",
"py-1.5 px-2",
isFocused
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
: isDraggingOver
@ -1068,7 +1071,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"resize-none",
"overflow-x-hidden",
"overflow-y-auto",
isEditMode ? "pr-20" : "pr-9",
"pr-9",
"flex-none flex-grow",
"z-[2]",
"scrollbar-none",
@ -1077,7 +1080,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onScroll={() => updateHighlights()}
/>
<div className="absolute top-2 right-2 z-30">
<div className="absolute top-1 right-1 z-30">
<StandardTooltip content={t("chat:enhancePrompt")}>
<button
aria-label={t("chat:enhancePrompt")}
@ -1099,7 +1102,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</StandardTooltip>
</div>
<div className="absolute bottom-2 right-2 z-30">
<div className="absolute bottom-1 right-1 z-30">
{isEditMode && (
<StandardTooltip content={t("chat:cancel.title")}>
<button
@ -1144,12 +1147,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
{!inputValue && (
<div
className={cn(
"absolute left-2 z-30 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height",
isEditMode ? "pr-20" : "pr-9",
)}
className="absolute left-2 z-30 pr-9 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height"
style={{
bottom: "0.75rem",
bottom: "0.25rem",
color: "color-mix(in oklab, var(--vscode-input-foreground) 50%, transparent)",
userSelect: "none",
pointerEvents: "none",

View file

@ -3,10 +3,10 @@ import { useDeepCompareEffect, useEvent, useMount } from "react-use"
import debounce from "debounce"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import removeMd from "remove-markdown"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import useSound from "use-sound"
import { LRUCache } from "lru-cache"
import { Trans, useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
import { appendImages } from "@src/utils/imageUtils"
@ -37,10 +37,10 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import RooHero from "@src/components/welcome/RooHero"
import RooTips from "@src/components/welcome/RooTips"
import RooCloudCTA from "@src/components/welcome/RooCloudCTA"
import { StandardTooltip } from "@src/components/ui"
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
import TelemetryBanner from "../common/TelemetryBanner"
import VersionIndicator from "../common/VersionIndicator"
@ -56,9 +56,6 @@ import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning"
import { CheckpointWarning } from "./CheckpointWarning"
import { QueuedMessages } from "./QueuedMessages"
import DismissibleUpsell from "../common/DismissibleUpsell"
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
import { Cloud } from "lucide-react"
export interface ChatViewProps {
isHidden: boolean
@ -211,15 +208,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
clineAskRef.current = clineAsk
}, [clineAsk])
const {
isOpen: isUpsellOpen,
openUpsell,
closeUpsell,
handleConnect,
} = useCloudUpsell({
autoOpenOnAuth: false,
})
// Keep inputValueRef in sync with inputValue state
useEffect(() => {
inputValueRef.current = inputValue
@ -1777,7 +1765,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
<div
data-testid="chat-view"
className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
{telemetrySetting === "unset" && <TelemetryBanner />}
{(showAnnouncement || showAnnouncementModal) && (
<Announcement
hideAnnouncement={() => {
@ -1841,27 +1828,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
/>
<RooHero />
{telemetrySetting === "unset" && <TelemetryBanner />}
<div className="mb-2.5">
{cloudIsAuthenticated || taskHistory.length < 4 ? (
<RooTips />
) : (
<>
<DismissibleUpsell
upsellId="taskList"
icon={<Cloud className="size-4 mt-0.5 shrink-0" />}
onClick={() => openUpsell()}
dismissOnClick={false}
className="bg-vscode-editor-background p-4 !text-base">
<Trans
i18nKey="cloud:upsell.taskList"
components={{
learnMoreLink: <VSCodeLink href="#" />,
}}
/>
</DismissibleUpsell>
</>
)}
{cloudIsAuthenticated || taskHistory.length < 4 ? <RooTips /> : <RooCloudCTA />}
</div>
{/* Show the task history preview if expanded and tasks exist */}
{taskHistory.length > 0 && isExpanded && <HistoryPreview />}
@ -2043,7 +2013,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
)}
<div id="roo-portal" />
<CloudUpsellDialog open={isUpsellOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
</div>
)
}

View file

@ -1,131 +0,0 @@
import { useState, useEffect, useCallback } from "react"
import { useTranslation } from "react-i18next"
import { CloudUpload, Copy, Check } from "lucide-react"
import QRCode from "qrcode"
import type { HistoryItem } from "@roo-code/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useCopyToClipboard } from "@/utils/clipboard"
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, StandardTooltip } from "@/components/ui"
import { vscode } from "@/utils/vscode"
interface CloudTaskButtonProps {
item?: HistoryItem
disabled?: boolean
}
export const CloudTaskButton = ({ item, disabled = false }: CloudTaskButtonProps) => {
const [dialogOpen, setDialogOpen] = useState(false)
const { t } = useTranslation()
const { cloudUserInfo, cloudApiUrl } = useExtensionState()
const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard()
const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | null>(null)
// Generate the cloud URL for the task
const cloudTaskUrl = item?.id ? `${cloudApiUrl}/task/${item.id}` : ""
const generateQRCode = useCallback(
(canvas: HTMLCanvasElement, context: string) => {
if (!cloudTaskUrl) {
// This will run again later when ready
return
}
QRCode.toCanvas(
canvas,
cloudTaskUrl,
{
width: 140,
margin: 0,
color: {
dark: "#000000",
light: "#FFFFFF",
},
},
(error: Error | null | undefined) => {
if (error) {
console.error(`Error generating QR code (${context}):`, error)
}
},
)
},
[cloudTaskUrl],
)
// Callback ref to capture canvas element when it mounts
const canvasRef = useCallback(
(node: HTMLCanvasElement | null) => {
if (node) {
setCanvasElement(node)
// Try to generate QR code immediately when canvas is available
if (dialogOpen) {
generateQRCode(node, "on mount")
}
} else {
setCanvasElement(null)
}
},
[dialogOpen, generateQRCode],
)
// Also generate QR code when dialog opens after canvas is available
useEffect(() => {
if (dialogOpen && canvasElement) {
generateQRCode(canvasElement, "in useEffect")
}
}, [dialogOpen, canvasElement, generateQRCode])
if (!cloudUserInfo?.extensionBridgeEnabled || !item?.id) {
return null
}
return (
<>
<StandardTooltip content={t("chat:task.openInCloud")}>
<Button
variant="ghost"
size="icon"
disabled={disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={() => setDialogOpen(true)}
data-testid="cloud-task-button"
aria-label={t("chat:task.openInCloud")}>
<CloudUpload className="h-4 w-4" />
</Button>
</StandardTooltip>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-100">
<DialogHeader>
<DialogTitle>{t("chat:task.openInCloud")}</DialogTitle>
</DialogHeader>
<div className="flex flex-col space-y-4">
<p className="text-center md:text-left max-w-80">{t("chat:task.openInCloudIntro")}</p>
<div className="flex justify-center md:justify-start">
<div
className="w-[170px] h-[170px] bg-white rounded-lg border-border cursor-pointer hover:opacity-70 transition-opacity"
onClick={() => vscode.postMessage({ type: "openExternal", url: cloudTaskUrl })}
title={t("chat:task.openInCloud")}>
<canvas ref={canvasRef} className="m-[15px]" />
</div>
</div>
<div className="flex items-center space-x-2">
<Input value={cloudTaskUrl} disabled className="flex-1 font-mono text-sm" readOnly />
<Button
variant="outline"
size="icon"
onClick={(e) => copyWithFeedback(cloudTaskUrl, e)}
className="h-9 w-9">
{showCopyFeedback ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
)
}

Some files were not shown because too many files have changed in this diff Show more