Browse Source

Merge branch 'upstream-at-v3.28.2' into roo-v3.28.2

Kevin van Dijk 5 months ago
parent
commit
8d46977843
100 changed files with 3797 additions and 1031 deletions
  1. 1 1
      apps/web-roo-code/next.config.ts
  2. 24 101
      apps/web-roo-code/src/app/evals/evals.tsx
  3. 336 0
      apps/web-roo-code/src/app/evals/plot.tsx
  4. 9 0
      apps/web-roo-code/src/app/evals/types.ts
  5. 15 26
      apps/web-roo-code/src/components/chromes/nav-bar.tsx
  6. 1 0
      apps/web-roo-code/src/lib/constants.ts
  7. 7 1
      apps/web-roo-code/src/lib/format-currency.ts
  8. 5 1
      apps/web-roo-code/src/lib/format-duration.ts
  9. 5 1
      apps/web-roo-code/src/lib/format-tokens.ts
  10. 1 1
      apps/web-roo-code/src/lib/hooks/use-open-router-models.ts
  11. 5 0
      packages/cloud/src/CloudService.ts
  12. 15 0
      packages/cloud/src/CloudSettingsService.ts
  13. 7 0
      packages/cloud/src/StaticSettingsService.ts
  14. 2 2
      packages/cloud/src/TelemetryClient.ts
  15. 8 0
      packages/cloud/src/__tests__/CloudService.test.ts
  16. 189 0
      packages/cloud/src/__tests__/CloudSettingsService.test.ts
  17. 23 0
      packages/cloud/src/__tests__/StaticSettingsService.test.ts
  18. 20 63
      packages/cloud/src/__tests__/TelemetryClient.test.ts
  19. 19 4
      packages/cloud/src/bridge/BridgeOrchestrator.ts
  20. 2 0
      packages/evals/package.json
  21. 6 0
      packages/evals/src/db/migrations/0002_bouncy_blazing_skull.sql
  22. 453 0
      packages/evals/src/db/migrations/meta/0002_snapshot.json
  23. 7 0
      packages/evals/src/db/migrations/meta/_journal.json
  24. 6 0
      packages/evals/src/db/schema.ts
  25. 3 3
      packages/telemetry/src/TelemetryService.ts
  26. 1 1
      packages/types/npm/package.metadata.json
  27. 9 0
      packages/types/src/cloud.ts
  28. 1 4
      packages/types/src/global-settings.ts
  29. 25 23
      packages/types/src/providers/chutes.ts
  30. 8 8
      packages/types/src/providers/deepseek.ts
  31. 55 0
      packages/types/src/providers/vertex.ts
  32. 1 1
      packages/types/src/telemetry.ts
  33. 149 287
      pnpm-lock.yaml
  34. BIN
      releases/3.28.0-release.png
  35. BIN
      releases/3.28.1-release.png
  36. BIN
      releases/3.28.2-release.png
  37. 257 0
      src/__tests__/extension.spec.ts
  38. 5 0
      src/api/providers/__tests__/chutes.spec.ts
  39. 1 0
      src/api/providers/__tests__/fireworks.spec.ts
  40. 2 79
      src/api/providers/__tests__/groq.spec.ts
  41. 1 66
      src/api/providers/__tests__/openai.spec.ts
  42. 3 3
      src/api/providers/__tests__/roo.spec.ts
  43. 1 0
      src/api/providers/__tests__/sambanova.spec.ts
  44. 1 0
      src/api/providers/__tests__/zai.spec.ts
  45. 3 8
      src/api/providers/base-openai-compatible-provider.ts
  46. 74 6
      src/api/providers/fetchers/__tests__/ollama.test.ts
  47. 1 1
      src/api/providers/fetchers/modelCache.ts
  48. 18 5
      src/api/providers/fetchers/ollama.ts
  49. 1 12
      src/api/providers/groq.ts
  50. 1 1
      src/api/providers/native-ollama.ts
  51. 1 11
      src/api/providers/openai.ts
  52. 260 0
      src/core/condense/__tests__/condense.spec.ts
  53. 12 8
      src/core/condense/__tests__/index.spec.ts
  54. 25 7
      src/core/condense/index.ts
  55. 37 3
      src/core/task/Task.ts
  56. 1 0
      src/core/tools/__tests__/applyDiffTool.experiment.spec.ts
  57. 54 0
      src/core/tools/__tests__/generateImageTool.test.ts
  58. 1 0
      src/core/tools/__tests__/multiApplyDiffTool.spec.ts
  59. 5 0
      src/core/tools/applyDiffTool.ts
  60. 5 1
      src/core/tools/generateImageTool.ts
  61. 3 0
      src/core/tools/insertContentTool.ts
  62. 5 0
      src/core/tools/multiApplyDiffTool.ts
  63. 3 0
      src/core/tools/searchAndReplaceTool.ts
  64. 3 0
      src/core/tools/writeToFileTool.ts
  65. 42 3
      src/core/webview/ClineProvider.ts
  66. 23 36
      src/core/webview/__tests__/ClineProvider.spec.ts
  67. 245 0
      src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts
  68. 390 0
      src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts
  69. 42 0
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  70. 193 45
      src/core/webview/webviewMessageHandler.ts
  71. 7 34
      src/extension.ts
  72. 22 2
      src/i18n/locales/ca/common.json
  73. 16 1
      src/i18n/locales/de/common.json
  74. 16 1
      src/i18n/locales/en/common.json
  75. 16 1
      src/i18n/locales/es/common.json
  76. 23 3
      src/i18n/locales/fr/common.json
  77. 23 3
      src/i18n/locales/hi/common.json
  78. 23 3
      src/i18n/locales/id/common.json
  79. 23 3
      src/i18n/locales/it/common.json
  80. 23 3
      src/i18n/locales/ja/common.json
  81. 23 3
      src/i18n/locales/ko/common.json
  82. 23 3
      src/i18n/locales/nl/common.json
  83. 23 3
      src/i18n/locales/pl/common.json
  84. 23 3
      src/i18n/locales/pt-BR/common.json
  85. 23 3
      src/i18n/locales/ru/common.json
  86. 23 3
      src/i18n/locales/tr/common.json
  87. 23 3
      src/i18n/locales/vi/common.json
  88. 23 3
      src/i18n/locales/zh-CN/common.json
  89. 22 2
      src/i18n/locales/zh-TW/common.json
  90. 42 17
      src/services/checkpoints/ShadowCheckpointService.ts
  91. 8 5
      src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts
  92. 7 2
      src/shared/ExtensionMessage.ts
  93. 6 0
      src/shared/WebviewMessage.ts
  94. 72 9
      src/shared/__tests__/context-mentions.spec.ts
  95. 1 1
      src/shared/api.ts
  96. 16 9
      src/shared/context-mentions.ts
  97. 2 0
      webview-ui/package.json
  98. 2 2
      webview-ui/src/App.tsx
  99. 105 75
      webview-ui/src/components/chat/Announcement.tsx
  100. 1 8
      webview-ui/src/components/chat/ApiConfigSelector.tsx

+ 1 - 1
apps/web-roo-code/next.config.ts

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

+ 24 - 101
apps/web-roo-code/src/app/evals/evals.tsx

@@ -1,59 +1,33 @@
 "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 {
-	ChartContainer,
-	ChartTooltip,
-	ChartTooltipContent,
-	ChartConfig,
-	ChartLegend,
-	ChartLegendContent,
-	Table,
-	TableBody,
-	TableCaption,
-	TableCell,
-	TableHead,
-	TableHeader,
-	TableRow,
-} from "@/components/ui"
+import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
+
+import type { EvalRun } from "./types"
+import { Plot } from "./plot"
 
-export function Evals({
-	runs,
-}: {
-	runs: (Run & {
-		label: string
-		score: number
-		languageScores?: Record<"go" | "java" | "javascript" | "python" | "rust", number>
-		taskMetrics: TaskMetrics
-		modelId?: string
-	})[]
-}) {
+export function Evals({ runs }: { runs: EvalRun[] }) {
 	const { data: openRouterModels } = useOpenRouterModels()
 
-	const tableData = useMemo(
+	const tableData: (EvalRun & { label: string; cost: number })[] = useMemo(
 		() =>
-			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],
-	)
+			runs.map((run) => {
+				const openRouterModelInfo = openRouterModels?.[run.modelId ?? ""]?.modelInfo
 
-	const chartData = useMemo(() => tableData.filter(({ cost }) => cost < 100), [tableData])
-
-	const chartConfig = useMemo(
-		() => chartData.reduce((acc, run) => ({ ...acc, [run.label]: run }), {} as ChartConfig),
-		[chartData],
+				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, openRouterModels],
 	)
 
 	return (
@@ -127,17 +101,15 @@ export function Evals({
 				<TableBody className="font-mono">
 					{tableData.map((run) => (
 						<TableRow key={run.id}>
-							<TableCell title={run.model?.description}>
+							<TableCell title={run.description ?? undefined}>
 								<div className="font-sans">{run.label}</div>
-								<div className="text-xs opacity-50">
-									{formatTokens(run.modelInfo?.contextWindow ?? 0)}
-								</div>
+								<div className="text-xs opacity-50">{formatTokens(run.contextWindow)}</div>
 							</TableCell>
 							<TableCell className="border-r">
 								<div className="flex flex-row gap-2">
-									<div>{formatCurrency(run.modelInfo?.inputPrice ?? 0)}</div>
+									<div>{formatCurrency(run.inputPrice)}</div>
 									<div className="opacity-25">/</div>
-									<div>{formatCurrency(run.modelInfo?.outputPrice ?? 0)}</div>
+									<div>{formatCurrency(run.outputPrice)}</div>
 								</div>
 							</TableCell>
 							<TableCell className="font-mono">{formatDuration(run.taskMetrics.duration)}</TableCell>
@@ -169,58 +141,9 @@ export function Evals({
 					))}
 				</TableBody>
 				<TableCaption>
-					<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>
+					<Plot tableData={tableData} />
 				</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}
-	/>
-)

+ 336 - 0
apps/web-roo-code/src/app/evals/plot.tsx

@@ -0,0 +1,336 @@
+"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}%)`
+}

+ 9 - 0
apps/web-roo-code/src/app/evals/types.ts

@@ -0,0 +1,9 @@
+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
+}

+ 15 - 26
apps/web-roo-code/src/components/chromes/nav-bar.tsx

@@ -69,19 +69,13 @@ export function NavBar({ stars, downloads }: NavBarProps) {
 						className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
 						Community
 					</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>
+					<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>
 				</nav>
 
 				<div className="hidden md:flex md:items-center md:space-x-4">
@@ -121,19 +115,6 @@ 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"
@@ -181,6 +162,14 @@ 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" />
 

+ 1 - 0
apps/web-roo-code/src/lib/constants.ts

@@ -24,6 +24,7 @@ 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 = {

+ 7 - 1
apps/web-roo-code/src/lib/format-currency.ts

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

+ 5 - 1
apps/web-roo-code/src/lib/format-duration.ts

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

+ 5 - 1
apps/web-roo-code/src/lib/format-tokens.ts

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

+ 1 - 1
apps/web-roo-code/src/lib/hooks/use-open-router-models.ts

@@ -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))

+ 5 - 0
packages/cloud/src/CloudService.ts

@@ -248,6 +248,11 @@ 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 {

+ 15 - 0
packages/cloud/src/CloudSettingsService.ts

@@ -266,6 +266,21 @@ 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

+ 7 - 0
packages/cloud/src/StaticSettingsService.ts

@@ -51,6 +51,7 @@ export class StaticSettingsService implements SettingsService {
 			},
 			settings: {
 				extensionBridgeEnabled: true,
+				taskSyncEnabled: true,
 			},
 			version: 1,
 		}
@@ -65,6 +66,7 @@ export class StaticSettingsService implements SettingsService {
 	public getUserSettingsConfig(): UserSettingsConfig {
 		return {
 			extensionBridgeEnabled: true,
+			taskSyncEnabled: true,
 		}
 	}
 
@@ -72,6 +74,11 @@ 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.
 	}

+ 2 - 2
packages/cloud/src/TelemetryClient.ts

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

+ 8 - 0
packages/cloud/src/__tests__/CloudService.test.ts

@@ -59,6 +59,7 @@ 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>
@@ -130,6 +131,7 @@ 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(),
@@ -343,6 +345,12 @@ 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", () => {

+ 189 - 0
packages/cloud/src/__tests__/CloudSettingsService.test.ts

@@ -20,6 +20,7 @@ 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>
@@ -63,6 +64,7 @@ describe("CloudSettingsService", () => {
 			getSessionToken: vi.fn(),
 			hasActiveSession: vi.fn().mockReturnValue(false),
 			on: vi.fn(),
+			getStoredOrganizationId: vi.fn().mockReturnValue(null),
 		}
 
 		mockRefreshTimer = {
@@ -532,4 +534,191 @@ 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)
+		})
+	})
 })

+ 23 - 0
packages/cloud/src/__tests__/StaticSettingsService.test.ts

@@ -98,5 +98,28 @@ 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)
+			})
+		})
 	})
 })

+ 20 - 63
packages/cloud/src/__tests__/TelemetryClient.test.ts

@@ -36,6 +36,14 @@ describe.skip("TelemetryClient", () => {
 					recordTaskMessages: true,
 				},
 			}),
+			getUserSettings: vi.fn().mockReturnValue({
+				features: {},
+				settings: {
+					taskSyncEnabled: true,
+				},
+				version: 1,
+			}),
+			isTaskSyncEnabled: vi.fn().mockReturnValue(true),
 		}
 
 		mockFetch.mockResolvedValue({
@@ -77,12 +85,8 @@ describe.skip("TelemetryClient", () => {
 			expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
 		})
 
-		it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => {
-			mockSettingsService.getSettings.mockReturnValue({
-				cloudSettings: {
-					recordTaskMessages: true,
-				},
-			})
+		it("should return true for TASK_MESSAGE events when isTaskSyncEnabled returns true", () => {
+			mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
 
 			const client = new TelemetryClient(mockAuthService, mockSettingsService)
 
@@ -92,55 +96,11 @@ describe.skip("TelemetryClient", () => {
 			).bind(client)
 
 			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
+			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
 		})
 
-		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)
+		it("should return false for TASK_MESSAGE events when isTaskSyncEnabled returns false", () => {
+			mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
 
 			const client = new TelemetryClient(mockAuthService, mockSettingsService)
 
@@ -150,6 +110,7 @@ describe.skip("TelemetryClient", () => {
 			).bind(client)
 
 			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
+			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
 		})
 	})
 
@@ -274,10 +235,8 @@ describe.skip("TelemetryClient", () => {
 			expect(mockFetch).not.toHaveBeenCalled()
 		})
 
-		it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => {
-			mockSettingsService.getSettings.mockReturnValue({
-				cloudSettings: {},
-			})
+		it("should not capture TASK_MESSAGE events when isTaskSyncEnabled returns false", async () => {
+			mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
 
 			const client = new TelemetryClient(mockAuthService, mockSettingsService)
 
@@ -295,6 +254,7 @@ describe.skip("TelemetryClient", () => {
 			})
 
 			expect(mockFetch).not.toHaveBeenCalled()
+			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
 		})
 
 		it("should not send request when schema validation fails", async () => {
@@ -354,12 +314,8 @@ describe.skip("TelemetryClient", () => {
 			)
 		})
 
-		it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => {
-			mockSettingsService.getSettings.mockReturnValue({
-				cloudSettings: {
-					recordTaskMessages: true,
-				},
-			})
+		it("should attempt to capture TASK_MESSAGE events when isTaskSyncEnabled returns true", async () => {
+			mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
 
 			const eventProperties = {
 				appName: "roo-code",
@@ -390,6 +346,7 @@ describe.skip("TelemetryClient", () => {
 				properties: eventProperties,
 			})
 
+			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
 			expect(mockFetch).toHaveBeenCalledWith(
 				"https://app.roocode.com/api/events",
 				expect.objectContaining({

+ 19 - 4
packages/cloud/src/bridge/BridgeOrchestrator.ts

@@ -59,13 +59,28 @@ export class BridgeOrchestrator {
 		return BridgeOrchestrator.instance
 	}
 
-	public static isEnabled(user?: CloudUserInfo | null, remoteControlEnabled?: boolean): boolean {
-		return !!(user?.id && user.extensionBridgeEnabled && remoteControlEnabled)
+	public static isEnabled(user: CloudUserInfo | null, remoteControlEnabled: boolean): boolean {
+		// Always disabled if signed out.
+		if (!user) {
+			return false
+		}
+
+		// Disabled by the user's organization?
+		if (!user.extensionBridgeEnabled) {
+			return false
+		}
+
+		// Disabled by the user?
+		if (!remoteControlEnabled) {
+			return false
+		}
+
+		return true
 	}
 
 	public static async connectOrDisconnect(
-		userInfo: CloudUserInfo | null,
-		remoteControlEnabled: boolean | undefined,
+		userInfo: CloudUserInfo,
+		remoteControlEnabled: boolean,
 		options: BridgeOrchestratorOptions,
 	): Promise<void> {
 		if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) {

+ 2 - 0
packages/evals/package.json

@@ -15,6 +15,8 @@
 		"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",

+ 6 - 0
packages/evals/src/db/migrations/0002_bouncy_blazing_skull.sql

@@ -0,0 +1,6 @@
+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;

+ 453 - 0
packages/evals/src/db/migrations/meta/0002_snapshot.json

@@ -0,0 +1,453 @@
+{
+	"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": {}
+	}
+}

+ 7 - 0
packages/evals/src/db/migrations/meta/_journal.json

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

+ 6 - 0
packages/evals/src/db/schema.ts

@@ -13,7 +13,13 @@ 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(),

+ 3 - 3
packages/telemetry/src/TelemetryService.ts

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

+ 1 - 1
packages/types/npm/package.metadata.json

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

+ 9 - 0
packages/types/src/cloud.ts

@@ -162,6 +162,7 @@ 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>
@@ -302,6 +303,14 @@ 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
 	 */

+ 1 - 4
packages/types/src/global-settings.ts

@@ -42,6 +42,7 @@ 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(),
@@ -151,8 +152,6 @@ export const globalSettingsSchema = z.object({
 	enableMcpServerCreation: z.boolean().optional(),
 	mcpMarketplaceCatalog: z.any().optional(), // kilocode_change: MCP marketplace catalog
 
-	remoteControlEnabled: z.boolean().optional(),
-
 	mode: z.string().optional(),
 	modeApiConfigs: z.record(z.string(), z.string()).optional(),
 	customModes: z.array(modeConfigSchema).optional(),
@@ -340,8 +339,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 
 	mcpEnabled: false,
 
-	remoteControlEnabled: false,
-
 	mode: "code", // "architect",
 
 	customModes: [],

+ 25 - 23
packages/types/src/providers/chutes.ts

@@ -20,20 +20,20 @@ export type ChutesModelId =
 	| "deepseek-ai/DeepSeek-V3-0324"
 	| "Qwen/Qwen3-235B-A22B"
 	| "Qwen/Qwen3-235B-A22B-Instruct-2507"
-	| "Qwen/Qwen3-235B-A22B-Thinking-2507"
-	| "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8"
-	| "Qwen/Qwen3-Next-80B-A3B-Instruct"
-	| "Qwen/Qwen3-Next-80B-A3B-Thinking"
 	| "Qwen/Qwen3-32B"
 	| "Qwen/Qwen3-30B-A3B"
 	| "Qwen/Qwen3-14B"
 	| "Qwen/Qwen3-8B"
+	| "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8"
 	| "microsoft/MAI-DS-R1-FP8"
 	| "tngtech/DeepSeek-R1T-Chimera"
 	| "zai-org/GLM-4.5-Air"
 	| "zai-org/GLM-4.5-FP8"
 	| "moonshotai/Kimi-K2-Instruct-75k"
 	| "moonshotai/Kimi-K2-Instruct-0905"
+	| "Qwen/Qwen3-235B-A22B-Thinking-2507"
+	| "Qwen/Qwen3-Next-80B-A3B-Instruct"
+	| "Qwen/Qwen3-Next-80B-A3B-Thinking"
 
 export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1-0528"
 
@@ -182,23 +182,23 @@ export const chutesModels = {
 		outputPrice: 0,
 		description: "DeepSeek V3 (0324) model.",
 	},
-	"Qwen/Qwen3-235B-A22B": {
+	"Qwen/Qwen3-235B-A22B-Instruct-2507": {
 		maxTokens: 32768,
-		contextWindow: 40960,
+		contextWindow: 262144,
 		supportsImages: false,
 		supportsPromptCache: false,
 		inputPrice: 0,
 		outputPrice: 0,
-		description: "Qwen3 235B A22B model.",
+		description: "Qwen3 235B A22B Instruct 2507 model with 262K context window.",
 	},
-	"Qwen/Qwen3-235B-A22B-Instruct-2507": {
+	"Qwen/Qwen3-235B-A22B": {
 		maxTokens: 32768,
-		contextWindow: 262144,
+		contextWindow: 40960,
 		supportsImages: false,
 		supportsPromptCache: false,
 		inputPrice: 0,
 		outputPrice: 0,
-		description: "Qwen3 235B A22B Instruct 2507 model with 262K context window.",
+		description: "Qwen3 235B A22B model.",
 	},
 	"Qwen/Qwen3-32B": {
 		maxTokens: 32768,
@@ -274,6 +274,15 @@ export const chutesModels = {
 		description:
 			"GLM-4.5-FP8 model with 128k token context window, optimized for agent-based applications with MoE architecture.",
 	},
+	"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": {
+		maxTokens: 32768,
+		contextWindow: 262144,
+		supportsImages: false,
+		supportsPromptCache: false,
+		inputPrice: 0,
+		outputPrice: 0,
+		description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.",
+	},
 	"moonshotai/Kimi-K2-Instruct-75k": {
 		maxTokens: 32768,
 		contextWindow: 75000,
@@ -301,31 +310,24 @@ export const chutesModels = {
 		outputPrice: 0.31202496,
 		description: "Qwen3 235B A22B Thinking 2507 model with 262K context window.",
 	},
-	"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": {
-		maxTokens: 32768,
-		contextWindow: 262144,
-		supportsImages: false,
-		supportsPromptCache: false,
-		inputPrice: 0,
-		outputPrice: 0,
-		description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.",
-	},
 	"Qwen/Qwen3-Next-80B-A3B-Instruct": {
 		maxTokens: 32768,
-		contextWindow: 262144,
+		contextWindow: 131072,
 		supportsImages: false,
 		supportsPromptCache: false,
 		inputPrice: 0,
 		outputPrice: 0,
-		description: "Qwen3 Next 80B A3B Instruct model with 262K context window.",
+		description:
+			"Fast, stable instruction-tuned model optimized for complex tasks, RAG, and tool use without thinking traces.",
 	},
 	"Qwen/Qwen3-Next-80B-A3B-Thinking": {
 		maxTokens: 32768,
-		contextWindow: 262144,
+		contextWindow: 131072,
 		supportsImages: false,
 		supportsPromptCache: false,
 		inputPrice: 0,
 		outputPrice: 0,
-		description: "Qwen3 Next 80B A3B Thinking model with 262K context window.",
+		description:
+			"Reasoning-first model with structured thinking traces for multi-step problems, math proofs, and code synthesis.",
 	},
 } as const satisfies Record<string, ModelInfo>

+ 8 - 8
packages/types/src/providers/deepseek.ts

@@ -11,10 +11,10 @@ export const deepSeekModels = {
 		contextWindow: 128_000,
 		supportsImages: false,
 		supportsPromptCache: true,
-		inputPrice: 0.27, // $0.27 per million tokens (cache miss)
-		outputPrice: 1.1, // $1.10 per million tokens
-		cacheWritesPrice: 0.27, // $0.27 per million tokens (cache miss)
-		cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit).
+		inputPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025
+		outputPrice: 1.68, // $1.68 per million tokens - Updated Sept 5, 2025
+		cacheWritesPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025
+		cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit) - Updated Sept 5, 2025
 		description: `DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.`,
 	},
 	"deepseek-reasoner": {
@@ -22,10 +22,10 @@ export const deepSeekModels = {
 		contextWindow: 128_000,
 		supportsImages: false,
 		supportsPromptCache: true,
-		inputPrice: 0.55, // $0.55 per million tokens (cache miss)
-		outputPrice: 2.19, // $2.19 per million tokens
-		cacheWritesPrice: 0.55, // $0.55 per million tokens (cache miss)
-		cacheReadsPrice: 0.14, // $0.14 per million tokens (cache hit)
+		inputPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025
+		outputPrice: 1.68, // $1.68 per million tokens - Updated Sept 5, 2025
+		cacheWritesPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025
+		cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit) - Updated Sept 5, 2025
 		description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 64K output tokens.`,
 	},
 } as const satisfies Record<string, ModelInfo>

+ 55 - 0
packages/types/src/providers/vertex.ts

@@ -294,6 +294,60 @@ 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 = [
@@ -302,6 +356,7 @@ 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" },

+ 1 - 1
packages/types/src/telemetry.ts

@@ -272,11 +272,11 @@ export interface TelemetryClient {
 
 	setProvider(provider: TelemetryPropertiesProvider): void
 	capture(options: TelemetryEvent): Promise<void>
-	updateTelemetryState(didUserOptIn: boolean): void
 	// kilocode_change start
 	captureException(error: Error, properties?: Record<string | number, unknown>): void
 	updateIdentity(kilocodeToken: string): Promise<void>
 	// kilocode_change end
+	updateTelemetryState(isOptedIn: boolean): void
 	isTelemetryEnabled(): boolean
 	shutdown(): Promise<void>
 }

File diff suppressed because it is too large
+ 149 - 287
pnpm-lock.yaml


BIN
releases/3.28.0-release.png


BIN
releases/3.28.1-release.png


BIN
releases/3.28.2-release.png


+ 257 - 0
src/__tests__/extension.spec.ts

@@ -0,0 +1,257 @@
+// npx vitest run __tests__/extension.spec.ts
+
+import type * as vscode from "vscode"
+import type { AuthState } from "@roo-code/types"
+
+vi.mock("vscode", () => ({
+	window: {
+		createOutputChannel: vi.fn().mockReturnValue({
+			appendLine: vi.fn(),
+		}),
+		registerWebviewViewProvider: vi.fn(),
+		registerUriHandler: vi.fn(),
+		tabGroups: {
+			onDidChangeTabs: vi.fn(),
+		},
+		onDidChangeActiveTextEditor: vi.fn(),
+	},
+	workspace: {
+		registerTextDocumentContentProvider: vi.fn(),
+		getConfiguration: vi.fn().mockReturnValue({
+			get: vi.fn().mockReturnValue([]),
+		}),
+		createFileSystemWatcher: vi.fn().mockReturnValue({
+			onDidCreate: vi.fn(),
+			onDidChange: vi.fn(),
+			onDidDelete: vi.fn(),
+			dispose: vi.fn(),
+		}),
+		onDidChangeWorkspaceFolders: vi.fn(),
+	},
+	languages: {
+		registerCodeActionsProvider: vi.fn(),
+	},
+	commands: {
+		executeCommand: vi.fn(),
+	},
+	env: {
+		language: "en",
+	},
+	ExtensionMode: {
+		Production: 1,
+	},
+}))
+
+vi.mock("@dotenvx/dotenvx", () => ({
+	config: vi.fn(),
+}))
+
+const mockBridgeOrchestratorDisconnect = vi.fn().mockResolvedValue(undefined)
+
+vi.mock("@roo-code/cloud", () => ({
+	CloudService: {
+		createInstance: vi.fn(),
+		hasInstance: vi.fn().mockReturnValue(true),
+		get instance() {
+			return {
+				off: vi.fn(),
+				on: vi.fn(),
+				getUserInfo: vi.fn().mockReturnValue(null),
+				isTaskSyncEnabled: vi.fn().mockReturnValue(false),
+			}
+		},
+	},
+	BridgeOrchestrator: {
+		disconnect: mockBridgeOrchestratorDisconnect,
+	},
+	getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
+}))
+
+vi.mock("@roo-code/telemetry", () => ({
+	TelemetryService: {
+		createInstance: vi.fn().mockReturnValue({
+			register: vi.fn(),
+			setProvider: vi.fn(),
+			shutdown: vi.fn(),
+		}),
+		get instance() {
+			return {
+				register: vi.fn(),
+				setProvider: vi.fn(),
+				shutdown: vi.fn(),
+			}
+		},
+	},
+	PostHogTelemetryClient: vi.fn(),
+}))
+
+vi.mock("../utils/outputChannelLogger", () => ({
+	createOutputChannelLogger: vi.fn().mockReturnValue(vi.fn()),
+	createDualLogger: vi.fn().mockReturnValue(vi.fn()),
+}))
+
+vi.mock("../shared/package", () => ({
+	Package: {
+		name: "test-extension",
+		outputChannel: "Test Output",
+		version: "1.0.0",
+	},
+}))
+
+vi.mock("../shared/language", () => ({
+	formatLanguage: vi.fn().mockReturnValue("en"),
+}))
+
+vi.mock("../core/config/ContextProxy", () => ({
+	ContextProxy: {
+		getInstance: vi.fn().mockResolvedValue({
+			getValue: vi.fn(),
+			setValue: vi.fn(),
+			getValues: vi.fn().mockReturnValue({}),
+			getProviderSettings: vi.fn().mockReturnValue({}),
+		}),
+	},
+}))
+
+vi.mock("../integrations/editor/DiffViewProvider", () => ({
+	DIFF_VIEW_URI_SCHEME: "test-diff-scheme",
+}))
+
+vi.mock("../integrations/terminal/TerminalRegistry", () => ({
+	TerminalRegistry: {
+		initialize: vi.fn(),
+		cleanup: vi.fn(),
+	},
+}))
+
+vi.mock("../services/mcp/McpServerManager", () => ({
+	McpServerManager: {
+		cleanup: vi.fn().mockResolvedValue(undefined),
+		getInstance: vi.fn().mockResolvedValue(null),
+		unregisterProvider: vi.fn(),
+	},
+}))
+
+vi.mock("../services/code-index/manager", () => ({
+	CodeIndexManager: {
+		getInstance: vi.fn().mockReturnValue(null),
+	},
+}))
+
+vi.mock("../services/mdm/MdmService", () => ({
+	MdmService: {
+		createInstance: vi.fn().mockResolvedValue(null),
+	},
+}))
+
+vi.mock("../utils/migrateSettings", () => ({
+	migrateSettings: vi.fn().mockResolvedValue(undefined),
+}))
+
+vi.mock("../utils/autoImportSettings", () => ({
+	autoImportSettings: vi.fn().mockResolvedValue(undefined),
+}))
+
+vi.mock("../extension/api", () => ({
+	API: vi.fn().mockImplementation(() => ({})),
+}))
+
+vi.mock("../activate", () => ({
+	handleUri: vi.fn(),
+	registerCommands: vi.fn(),
+	registerCodeActions: vi.fn(),
+	registerTerminalActions: vi.fn(),
+	CodeActionProvider: vi.fn().mockImplementation(() => ({
+		providedCodeActionKinds: [],
+	})),
+}))
+
+vi.mock("../i18n", () => ({
+	initializeI18n: vi.fn(),
+}))
+
+describe("extension.ts", () => {
+	let mockContext: vscode.ExtensionContext
+	let authStateChangedHandler:
+		| ((data: { state: AuthState; previousState: AuthState }) => void | Promise<void>)
+		| undefined
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockBridgeOrchestratorDisconnect.mockClear()
+
+		mockContext = {
+			extensionPath: "/test/path",
+			globalState: {
+				get: vi.fn().mockReturnValue(undefined),
+				update: vi.fn(),
+			},
+			subscriptions: [],
+		} as unknown as vscode.ExtensionContext
+
+		authStateChangedHandler = undefined
+	})
+
+	test("authStateChangedHandler calls BridgeOrchestrator.disconnect when logged-out event fires", async () => {
+		const { CloudService, BridgeOrchestrator } = await import("@roo-code/cloud")
+
+		// Capture the auth state changed handler.
+		vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
+			if (handlers?.["auth-state-changed"]) {
+				authStateChangedHandler = handlers["auth-state-changed"]
+			}
+
+			return {
+				off: vi.fn(),
+				on: vi.fn(),
+				telemetryClient: null,
+			} as any
+		})
+
+		// Activate the extension.
+		const { activate } = await import("../extension")
+		await activate(mockContext)
+
+		// Verify handler was registered.
+		expect(authStateChangedHandler).toBeDefined()
+
+		// Trigger logout.
+		await authStateChangedHandler!({
+			state: "logged-out" as AuthState,
+			previousState: "logged-in" as AuthState,
+		})
+
+		// Verify BridgeOrchestrator.disconnect was called
+		expect(mockBridgeOrchestratorDisconnect).toHaveBeenCalled()
+	})
+
+	test("authStateChangedHandler does not call BridgeOrchestrator.disconnect for other states", async () => {
+		const { CloudService } = await import("@roo-code/cloud")
+
+		// Capture the auth state changed handler.
+		vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
+			if (handlers?.["auth-state-changed"]) {
+				authStateChangedHandler = handlers["auth-state-changed"]
+			}
+
+			return {
+				off: vi.fn(),
+				on: vi.fn(),
+				telemetryClient: null,
+			} as any
+		})
+
+		// Activate the extension.
+		const { activate } = await import("../extension")
+		await activate(mockContext)
+
+		// Trigger login.
+		await authStateChangedHandler!({
+			state: "logged-in" as AuthState,
+			previousState: "logged-out" as AuthState,
+		})
+
+		// Verify BridgeOrchestrator.disconnect was NOT called.
+		expect(mockBridgeOrchestratorDisconnect).not.toHaveBeenCalled()
+	})
+})

+ 5 - 0
src/api/providers/__tests__/chutes.spec.ts

@@ -453,6 +453,10 @@ describe("ChutesHandler", () => {
 						content: `${systemPrompt}\n${messages[0].content}`,
 					},
 				],
+				max_tokens: 32768,
+				temperature: 0.6,
+				stream: true,
+				stream_options: { include_usage: true },
 			}),
 		)
 	})
@@ -482,6 +486,7 @@ 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 },

+ 1 - 0
src/api/providers/__tests__/fireworks.spec.ts

@@ -373,6 +373,7 @@ 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 },

+ 2 - 79
src/api/providers/__tests__/groq.spec.ts

@@ -149,7 +149,7 @@ describe("GroqHandler", () => {
 		expect(firstChunk.done).toBe(false)
 		expect(firstChunk.value).toMatchObject({
 			type: "usage",
-			inputTokens: 70, // 100 total - 30 cached
+			inputTokens: 100,
 			outputTokens: 50,
 			cacheWriteTokens: 0,
 			cacheReadTokens: 30,
@@ -160,11 +160,7 @@ 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",
-			modelTemperature: 0.5, // Explicitly set temperature for this test
-		})
+		const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" })
 
 		mockCreate.mockImplementationOnce(() => {
 			return {
@@ -194,77 +190,4 @@ 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,
-		)
-	})
 })

+ 1 - 66
src/api/providers/__tests__/openai.spec.ts

@@ -316,71 +316,6 @@ 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", () => {
@@ -516,7 +451,7 @@ describe("OpenAiHandler", () => {
 					],
 					stream: true,
 					stream_options: { include_usage: true },
-					// temperature should be omitted when not set
+					temperature: 0,
 				},
 				{ path: "/models/chat/completions" },
 			)

+ 3 - 3
src/api/providers/__tests__/roo.spec.ts

@@ -354,7 +354,7 @@ describe("RooHandler", () => {
 	})
 
 	describe("temperature and model configuration", () => {
-		it("should omit temperature when not explicitly set", async () => {
+		it("should use default temperature of 0.7", 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.not.objectContaining({
-					temperature: expect.anything(),
+				expect.objectContaining({
+					temperature: 0.7,
 				}),
 				undefined,
 			)

+ 1 - 0
src/api/providers/__tests__/sambanova.spec.ts

@@ -144,6 +144,7 @@ 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 },

+ 1 - 0
src/api/providers/__tests__/zai.spec.ts

@@ -228,6 +228,7 @@ 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 },

+ 3 - 8
src/api/providers/base-openai-compatible-provider.ts

@@ -75,22 +75,17 @@ 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 &&
-			this.options.modelTemperature !== null // kilocode_change: some providers like Chutes don't like this
-		) {
-			params.temperature = this.options.modelTemperature
-		}
-
 		try {
 			return this.client.chat.completions.create(params, requestOptions)
 		} catch (error) {

+ 74 - 6
src/api/providers/fetchers/__tests__/ollama.test.ts

@@ -108,10 +108,10 @@ describe("Ollama Fetcher", () => {
 			const result = await getOllamaModels(baseUrl)
 
 			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
 
 			expect(mockedAxios.post).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
+			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName }, { headers: {} })
 
 			expect(typeof result).toBe("object")
 			expect(result).not.toBeInstanceOf(Array)
@@ -130,7 +130,7 @@ describe("Ollama Fetcher", () => {
 			const result = await getOllamaModels(baseUrl)
 
 			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
 			expect(mockedAxios.post).not.toHaveBeenCalled()
 			expect(result).toEqual({})
 		})
@@ -146,7 +146,7 @@ describe("Ollama Fetcher", () => {
 			const result = await getOllamaModels(baseUrl)
 
 			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
 			expect(mockedAxios.post).not.toHaveBeenCalled()
 			expect(consoleInfoSpy).toHaveBeenCalledWith(`Failed connecting to Ollama at ${baseUrl}`)
 			expect(result).toEqual({})
@@ -204,10 +204,10 @@ describe("Ollama Fetcher", () => {
 			const result = await getOllamaModels(baseUrl)
 
 			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
 
 			expect(mockedAxios.post).toHaveBeenCalledTimes(1)
-			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
+			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName }, { headers: {} })
 
 			expect(typeof result).toBe("object")
 			expect(result).not.toBeInstanceOf(Array)
@@ -217,5 +217,73 @@ describe("Ollama Fetcher", () => {
 			// Verify the model was parsed correctly despite null families
 			expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B")
 		})
+
+		it("should include Authorization header when API key is provided", async () => {
+			const baseUrl = "http://localhost:11434"
+			const apiKey = "test-api-key-123"
+			const modelName = "test-model:latest"
+
+			const mockApiTagsResponse = {
+				models: [
+					{
+						name: modelName,
+						model: modelName,
+						modified_at: "2025-06-03T09:23:22.610222878-04:00",
+						size: 14333928010,
+						digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
+						details: {
+							family: "llama",
+							families: ["llama"],
+							format: "gguf",
+							parameter_size: "23.6B",
+							parent_model: "",
+							quantization_level: "Q4_K_M",
+						},
+					},
+				],
+			}
+			const mockApiShowResponse = {
+				license: "Mock License",
+				modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
+				parameters: "num_ctx 4096\nstop_token <eos>",
+				template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
+				modified_at: "2025-06-03T09:23:22.610222878-04:00",
+				details: {
+					parent_model: "",
+					format: "gguf",
+					family: "llama",
+					families: ["llama"],
+					parameter_size: "23.6B",
+					quantization_level: "Q4_K_M",
+				},
+				model_info: {
+					"ollama.context_length": 4096,
+					"some.other.info": "value",
+				},
+				capabilities: ["completion"],
+			}
+
+			mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
+			mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
+
+			const result = await getOllamaModels(baseUrl, apiKey)
+
+			const expectedHeaders = { Authorization: `Bearer ${apiKey}` }
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: expectedHeaders })
+
+			expect(mockedAxios.post).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.post).toHaveBeenCalledWith(
+				`${baseUrl}/api/show`,
+				{ model: modelName },
+				{ headers: expectedHeaders },
+			)
+
+			expect(typeof result).toBe("object")
+			expect(result).not.toBeInstanceOf(Array)
+			expect(Object.keys(result).length).toBe(1)
+			expect(result[modelName]).toBeDefined()
+		})
 	})
 })

+ 1 - 1
src/api/providers/fetchers/modelCache.ts

@@ -101,7 +101,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 				models = cerebrasModels
 				break
 			case "ollama":
-				models = await getOllamaModels(options.baseUrl)
+				models = await getOllamaModels(options.baseUrl, options.apiKey)
 				break
 			case "lmstudio":
 				models = await getLMStudioModels(options.baseUrl)

+ 18 - 5
src/api/providers/fetchers/ollama.ts

@@ -72,7 +72,10 @@ export const parseOllamaModel = (
 	return modelInfo
 }
 
-export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promise<Record<string, ModelInfo>> {
+export async function getOllamaModels(
+	baseUrl = "http://localhost:11434",
+	apiKey?: string,
+): Promise<Record<string, ModelInfo>> {
 	const models: Record<string, ModelInfo> = {}
 
 	// clearing the input can leave an empty string; use the default in that case
@@ -83,7 +86,13 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
 			return models
 		}
 
-		const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`)
+		// Prepare headers with optional API key
+		const headers: Record<string, string> = {}
+		if (apiKey) {
+			headers["Authorization"] = `Bearer ${apiKey}`
+		}
+
+		const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { headers })
 		const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
 		let modelInfoPromises = []
 
@@ -91,9 +100,13 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
 			for (const ollamaModel of parsedResponse.data.models) {
 				modelInfoPromises.push(
 					axios
-						.post<OllamaModelInfoResponse>(`${baseUrl}/api/show`, {
-							model: ollamaModel.model,
-						})
+						.post<OllamaModelInfoResponse>(
+							`${baseUrl}/api/show`,
+							{
+								model: ollamaModel.model,
+							},
+							{ headers },
+						)
 						.then((ollamaModelInfo) => {
 							models[ollamaModel.name] = parseOllamaModel(
 								ollamaModelInfo.data,

+ 1 - 12
src/api/providers/groq.ts

@@ -66,20 +66,9 @@ 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: nonCachedInputTokens,
+			inputTokens,
 			outputTokens,
 			cacheWriteTokens,
 			cacheReadTokens,

+ 1 - 1
src/api/providers/native-ollama.ts

@@ -294,7 +294,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 	}
 
 	async fetchModel() {
-		this.models = await getOllamaModels(this.options.ollamaBaseUrl)
+		this.models = await getOllamaModels(this.options.ollamaBaseUrl, this.options.ollamaApiKey)
 		return this.models // kilocode_change
 	}
 

+ 1 - 11
src/api/providers/openai.ts

@@ -159,23 +159,13 @@ 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 &&
-				this.options.modelTemperature !== null // kilocode_change: some providers like Chutes don't like this
-			) {
-				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)
 

+ 260 - 0
src/core/condense/__tests__/condense.spec.ts

@@ -0,0 +1,260 @@
+// 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 the original first user message for context preservation, the summary, and messages after
+			expect(result[0].role).toBe("user")
+			expect(result[0].content).toBe("First message") // Preserves original first message
+			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 with original first message preserved
+			expect(result[0].role).toBe("user")
+			expect(result[0].content).toBe("First message") // Preserves original first message
+			expect(result[1]).toEqual(messages[3]) // Second summary
+			expect(result[2]).toEqual(messages[4])
+			expect(result[3]).toEqual(messages[5])
+		})
+	})
+})

+ 12 - 8
src/core/condense/__tests__/index.spec.ts

@@ -36,7 +36,7 @@ describe("getMessagesSinceLastSummary", () => {
 		expect(result).toEqual(messages)
 	})
 
-	it("should return messages since the last summary with prepended user message", () => {
+	it("should return messages since the last summary with original first user message", () => {
 		const messages: ApiMessage[] = [
 			{ role: "user", content: "Hello", ts: 1 },
 			{ role: "assistant", content: "Hi there", ts: 2 },
@@ -47,14 +47,14 @@ describe("getMessagesSinceLastSummary", () => {
 
 		const result = getMessagesSinceLastSummary(messages)
 		expect(result).toEqual([
-			{ role: "user", content: "Please continue from the following summary:", ts: 0 },
+			{ role: "user", content: "Hello", ts: 1 },
 			{ role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true },
 			{ role: "user", content: "How are you?", ts: 4 },
 			{ role: "assistant", content: "I'm good", ts: 5 },
 		])
 	})
 
-	it("should handle multiple summary messages and return since the last one with prepended user message", () => {
+	it("should handle multiple summary messages and return since the last one with original first user message", () => {
 		const messages: ApiMessage[] = [
 			{ role: "user", content: "Hello", ts: 1 },
 			{ role: "assistant", content: "First summary", ts: 2, isSummary: true },
@@ -65,7 +65,7 @@ describe("getMessagesSinceLastSummary", () => {
 
 		const result = getMessagesSinceLastSummary(messages)
 		expect(result).toEqual([
-			{ role: "user", content: "Please continue from the following summary:", ts: 0 },
+			{ role: "user", content: "Hello", ts: 1 },
 			{ role: "assistant", content: "Second summary", ts: 4, isSummary: true },
 			{ role: "user", content: "What's new?", ts: 5 },
 		])
@@ -188,11 +188,14 @@ describe("summarizeConversation", () => {
 		expect(maybeRemoveImageBlocks).toHaveBeenCalled()
 
 		// Verify the structure of the result
-		// The result should be: original messages (except last N) + summary + last N messages
-		expect(result.messages.length).toBe(messages.length + 1) // Original + summary
+		// 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])
 
 		// Check that the summary message was inserted correctly
-		const summaryMessage = result.messages[result.messages.length - N_MESSAGES_TO_KEEP - 1]
+		const summaryMessage = result.messages[1]
 		expect(summaryMessage.role).toBe("assistant")
 		expect(summaryMessage.content).toBe("This is a summary")
 		expect(summaryMessage.isSummary).toBe(true)
@@ -395,7 +398,8 @@ describe("summarizeConversation", () => {
 		)
 
 		// Should successfully summarize
-		expect(result.messages.length).toBe(messages.length + 1) // Original + summary
+		// 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.cost).toBe(0.03)
 		expect(result.summary).toBe("Concise summary")
 		expect(result.error).toBeUndefined()

+ 25 - 7
src/core/condense/index.ts

@@ -100,7 +100,11 @@ export async function summarizeConversation(
 	)
 
 	const response: SummarizeResponse = { messages, cost: 0, summary: "" }
-	const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
+
+	// 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))
 
 	if (messagesToSummarize.length <= 1) {
 		// kilocode_change start
@@ -190,7 +194,8 @@ export async function summarizeConversation(
 		isSummary: true,
 	}
 
-	const newMessages = [...messages.slice(0, -N_MESSAGES_TO_KEEP), summaryMessage, ...keepMessages]
+	// Reconstruct messages: [first message, summary, last N messages]
+	const newMessages = [firstMessage, 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
@@ -225,11 +230,24 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
 	const messagesSinceSummary = messages.slice(lastSummaryIndex)
 
 	// Bedrock requires the first message to be a user message.
+	// We preserve the original first message to maintain context.
 	// See https://github.com/RooCodeInc/Roo-Code/issues/4147
-	const userMessage: ApiMessage = {
-		role: "user",
-		content: "Please continue from the following summary:",
-		ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(),
+	if (messagesSinceSummary.length > 0 && messagesSinceSummary[0].role !== "user") {
+		// Get the original first message (should always be a user message with the task)
+		const originalFirstMessage = messages[0]
+		if (originalFirstMessage && originalFirstMessage.role === "user") {
+			// Use the original first message unchanged to maintain full context
+			return [originalFirstMessage, ...messagesSinceSummary]
+		} else {
+			// Fallback to generic message if no original first message exists (shouldn't happen)
+			const userMessage: ApiMessage = {
+				role: "user",
+				content: "Please continue from the following summary:",
+				ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(),
+			}
+			return [userMessage, ...messagesSinceSummary]
+		}
 	}
-	return [userMessage, ...messagesSinceSummary]
+
+	return messagesSinceSummary
 }

+ 37 - 3
src/core/task/Task.ts

@@ -878,9 +878,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			const message = this.messageQueueService.dequeueMessage()
 
 			if (message) {
-				setTimeout(async () => {
-					await this.submitUserMessage(message.text, message.images)
-				}, 0)
+				// 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)
+				}
 			}
 		}
 
@@ -3081,4 +3091,28 @@ 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)
+		}
+	}
 }

+ 1 - 0
src/core/tools/__tests__/applyDiffTool.experiment.spec.ts

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

+ 54 - 0
src/core/tools/__tests__/generateImageTool.test.ts

@@ -163,6 +163,60 @@ 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", () => {

+ 1 - 0
src/core/tools/__tests__/multiApplyDiffTool.spec.ts

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

+ 5 - 0
src/core/tools/applyDiffTool.ts

@@ -207,6 +207,7 @@ export async function applyDiffToolLegacy(
 
 				if (!didApprove) {
 					await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view
+					cline.processQueuedMessages()
 					return
 				}
 
@@ -245,11 +246,15 @@ 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
 	}
 }

+ 5 - 1
src/core/tools/generateImageTool.ts

@@ -264,7 +264,11 @@ export async function generateImageTool(
 			const fullImagePath = path.join(cline.cwd, finalPath)
 
 			// Convert to webview URI if provider is available
-			const imageUri = provider?.convertToWebviewUri?.(fullImagePath) ?? vscode.Uri.file(fullImagePath).toString()
+			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}`
 
 			// Send the image with the webview URI
 			await cline.say("image", JSON.stringify({ imageUri, imagePath: fullImagePath }))

+ 3 - 0
src/core/tools/insertContentTool.ts

@@ -187,6 +187,9 @@ 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()

+ 5 - 0
src/core/tools/multiApplyDiffTool.ts

@@ -172,6 +172,7 @@ 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") {
@@ -195,6 +196,7 @@ Original error: ${errorMessage}`
 			"args (or legacy 'path' and 'diff' parameters)",
 		)
 		pushToolResult(errorMsg)
+		cline.processQueuedMessages()
 		return
 	}
 
@@ -210,6 +212,7 @@ Original error: ${errorMessage}`
 					: "args (must contain at least one valid file element)",
 			),
 		)
+		cline.processQueuedMessages()
 		return
 	}
 
@@ -675,10 +678,12 @@ ${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
 	}
 }

+ 3 - 0
src/core/tools/searchAndReplaceTool.ts

@@ -263,6 +263,9 @@ 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()

+ 3 - 0
src/core/tools/writeToFileTool.ts

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

+ 42 - 3
src/core/webview/ClineProvider.ts

@@ -154,7 +154,7 @@ export class ClineProvider
 
 	public isViewLaunched = false
 	public settingsImportedAt?: number
-	public readonly latestAnnouncementId = "aug-25-2025-grok-code-fast" // Update for Grok Code Fast announcement
+	public readonly latestAnnouncementId = "sep-2025-roo-code-cloud" // Roo Code Cloud announcement
 	public readonly providerSettingsManager: ProviderSettingsManager
 	public readonly customModesManager: CustomModesManager
 
@@ -884,7 +884,7 @@ export class ClineProvider
 			fuzzyMatchThreshold,
 			experiments,
 			cloudUserInfo,
-			remoteControlEnabled,
+			taskSyncEnabled,
 		} = await this.getState()
 
 		const task = new Task({
@@ -902,7 +902,7 @@ export class ClineProvider
 			taskNumber: historyItem.number,
 			workspacePath: historyItem.workspace,
 			onCreated: this.taskCreationCallback,
-			enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled),
+			enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled),
 		})
 
 		await this.addClineToStack(task)
@@ -1856,11 +1856,13 @@ export class ClineProvider
 			includeDiagnosticMessages,
 			maxDiagnosticMessages,
 			includeTaskHistoryInEnhance,
+			taskSyncEnabled,
 			remoteControlEnabled,
 			openRouterImageApiKey,
 			kiloCodeImageApiKey,
 			openRouterImageGenerationSelectedModel,
 			openRouterUseMiddleOutTransform,
+			featureRoomoteControlEnabled,
 		} = await this.getState()
 
 		const telemetryKey = process.env.KILOCODE_POSTHOG_API_KEY
@@ -2016,11 +2018,13 @@ export class ClineProvider
 			includeDiagnosticMessages: includeDiagnosticMessages ?? true,
 			maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
 			includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
+			taskSyncEnabled,
 			remoteControlEnabled,
 			openRouterImageApiKey,
 			kiloCodeImageApiKey,
 			openRouterImageGenerationSelectedModel,
 			openRouterUseMiddleOutTransform,
+			featureRoomoteControlEnabled,
 		}
 	}
 
@@ -2108,6 +2112,16 @@ 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,
@@ -2233,6 +2247,7 @@ export class ClineProvider
 			includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
 			maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
 			includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
+			taskSyncEnabled,
 			remoteControlEnabled: (() => {
 				try {
 					const cloudSettings = CloudService.instance.getUserSettings()
@@ -2247,6 +2262,18 @@ export class ClineProvider
 			openRouterImageApiKey: stateValues.openRouterImageApiKey,
 			kiloCodeImageApiKey: stateValues.kiloCodeImageApiKey,
 			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
+				}
+			})(),
 		}
 	}
 
@@ -2369,7 +2396,19 @@ export class ClineProvider
 	}
 
 	public async remoteControlEnabled(enabled: boolean) {
+		if (!enabled) {
+			await BridgeOrchestrator.disconnect()
+			return
+		}
+
 		const userInfo = CloudService.instance.getUserInfo()
+
+		if (!userInfo) {
+			this.log("[ClineProvider#remoteControlEnabled] Failed to get user info, disconnecting")
+			await BridgeOrchestrator.disconnect()
+			return
+		}
+
 		const config = await CloudService.instance.cloudAPI?.bridgeConfig().catch(() => undefined)
 
 		if (!config) {

+ 23 - 36
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -582,6 +582,9 @@ describe("ClineProvider", () => {
 			diagnosticsEnabled: true,
 			openRouterImageApiKey: undefined,
 			openRouterImageGenerationSelectedModel: undefined,
+			remoteControlEnabled: false,
+			taskSyncEnabled: false,
+			featureRoomoteControlEnabled: false,
 		}
 
 		const message: ExtensionMessage = {
@@ -1360,19 +1363,11 @@ describe("ClineProvider", () => {
 				text: "Edited message content",
 			})
 
-			// Verify correct messages were kept (only messages before the edited one)
-			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
-				mockMessages[0],
-				mockMessages[1],
-				mockMessages[2],
-			])
+			// Verify correct messages were kept - delete from the preceding user message to truly replace it
+			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([])
 
-			// Verify correct API messages were kept (only messages before the edited one)
-			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
-				mockApiHistory[0],
-				mockApiHistory[1],
-				mockApiHistory[2],
-			])
+			// Verify correct API messages were kept
+			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([])
 
 			// The new flow calls webviewMessageHandler recursively with askResponse
 			// We need to verify the recursive call happened by checking if the handler was called again
@@ -3076,7 +3071,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.handleWebviewAskResponse = vi.fn()
+			mockCline.submitUserMessage = vi.fn()
 
 			await provider.addClineToStack(mockCline)
 			;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@@ -3106,9 +3101,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				text: "Edited message with preserved images",
 			})
 
-			// 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 }])
+			// 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)
 		})
 
 		test("handles editing messages with file attachments", async () => {
@@ -3130,7 +3127,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.handleWebviewAskResponse = vi.fn()
+			mockCline.submitUserMessage = vi.fn()
 
 			await provider.addClineToStack(mockCline)
 			;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@@ -3161,11 +3158,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
-			expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
-				"messageResponse",
-				"Edited message with file attachment",
-				undefined,
-			)
+			expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with file attachment", undefined)
 		})
 	})
 
@@ -3257,7 +3250,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("Error editing message: Connection lost")
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
 		})
 	})
 
@@ -3380,7 +3373,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				text: "Edited message",
 			})
 
-			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized")
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
 		})
 
 		describe("Malformed Requests and Invalid Formats", () => {
@@ -3604,7 +3597,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				// Verify cleanup was attempted before failure
 				expect(cleanupSpy).toHaveBeenCalled()
-				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed")
+				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
 			})
 
 			test("validates proper cleanup during failed delete operations", async () => {
@@ -3644,9 +3637,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				// Verify cleanup was attempted before failure
 				expect(cleanupSpy).toHaveBeenCalled()
-				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
-					"Error deleting message: Delete operation failed",
-				)
+				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_deleting_message")
 			})
 		})
 
@@ -3669,7 +3660,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.handleWebviewAskResponse = vi.fn()
+				mockCline.submitUserMessage = vi.fn()
 
 				await provider.addClineToStack(mockCline)
 				;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@@ -3698,11 +3689,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
 
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
-				expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
-					"messageResponse",
-					largeEditedContent,
-					undefined,
-				)
+				expect(mockCline.submitUserMessage).toHaveBeenCalledWith(largeEditedContent, undefined)
 			})
 
 			test("handles deleting messages with large payloads", async () => {
@@ -3882,7 +3869,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				] as any[]
 				mockCline.overwriteClineMessages = vi.fn()
 				mockCline.overwriteApiConversationHistory = vi.fn()
-				mockCline.handleWebviewAskResponse = vi.fn()
+				mockCline.submitUserMessage = vi.fn()
 
 				await provider.addClineToStack(mockCline)
 				;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
@@ -3915,7 +3902,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				// Should handle future timestamps correctly
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
-				expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled()
+				expect(mockCline.submitUserMessage).toHaveBeenCalled()
 			})
 		})
 	})

+ 245 - 0
src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts

@@ -0,0 +1,245 @@
+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" } },
+			])
+		})
+	})
+})

+ 390 - 0
src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts

@@ -0,0 +1,390 @@
+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([])
+	})
+})

+ 42 - 0
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -137,6 +137,48 @@ 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()

+ 193 - 45
src/core/webview/webviewMessageHandler.ts

@@ -15,6 +15,7 @@ import {
 	type TelemetrySetting,
 	TelemetryEventName,
 	ghostServiceSettingsSchema, // kilocode_change
+	UserSettingsConfig,
 } from "@roo-code/types"
 import { CloudService } from "@roo-code/cloud"
 import { TelemetryService } from "@roo-code/telemetry"
@@ -95,6 +96,17 @@ 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
 	 */
@@ -120,18 +132,20 @@ 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,
-				)
 
-				hasCheckpoint = checkpoints.length > 0
-			} else {
-				console.log("[webviewMessageHandler] Message not found! Looking for 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
 		}
 
 		// Send message to webview to show delete confirmation dialog
@@ -153,11 +167,15 @@ 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) {
-			const errorMessage = `Message with timestamp ${messageTs} not found`
-			console.error("[handleDeleteMessageConfirm]", errorMessage)
-			await vscode.window.showErrorMessage(errorMessage)
+			await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs }))
 			return
 		}
 
@@ -199,7 +217,7 @@ export const webviewMessageHandler = async (
 				}
 
 				// Delete this message and all subsequent messages
-				await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
+				await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse)
 
 				// Restore checkpoint associations for preserved messages
 				for (const [ts, checkpoint] of preservedCheckpoints) {
@@ -215,11 +233,16 @@ 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(
-				`Error deleting message: ${error instanceof Error ? error.message : String(error)}`,
+				t("common:errors.message.error_deleting_message", {
+					error: error instanceof Error ? error.message : String(error),
+				}),
 			)
 		}
 	}
@@ -276,7 +299,7 @@ export const webviewMessageHandler = async (
 		const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
 
 		if (messageIndex === -1) {
-			const errorMessage = `Message with timestamp ${messageTs} not found`
+			const errorMessage = t("common:errors.message.message_not_found", { messageTs })
 			console.error("[handleEditMessageConfirm]", errorMessage)
 			await vscode.window.showErrorMessage(errorMessage)
 			return
@@ -319,18 +342,49 @@ export const webviewMessageHandler = async (
 				}
 			}
 
-			// For non-checkpoint edits, preserve checkpoint associations for remaining messages
+			// 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)
+				}
+			}
+
 			// Store checkpoints from messages that will be preserved
 			const preservedCheckpoints = new Map<number, any>()
-			for (let i = 0; i < messageIndex; i++) {
+			for (let i = 0; i < deleteFromMessageIndex; i++) {
 				const msg = currentCline.clineMessages[i]
 				if (msg?.checkpoint && msg.ts) {
 					preservedCheckpoints.set(msg.ts, msg.checkpoint)
 				}
 			}
 
-			// Edit this message and delete subsequent
-			await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
+			// Delete the original (user) message and all subsequent messages
+			await removeMessagesThisAndSubsequent(currentCline, deleteFromMessageIndex, deleteFromApiIndex)
 
 			// Restore checkpoint associations for preserved messages
 			for (const [ts, checkpoint] of preservedCheckpoints) {
@@ -347,20 +401,16 @@ export const webviewMessageHandler = async (
 				globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
 			})
 
-			// Process the edited message as a regular user message
-			webviewMessageHandler(provider, {
-				type: "askResponse",
-				askResponse: "messageResponse",
-				text: editedContent,
-				images,
-			})
+			// Update the UI to reflect the deletion
+			await provider.postStateToWebview()
 
-			// Don't initialize with history item for edit operations
-			// The webviewMessageHandler will handle the conversation state
+			await currentCline.submitUserMessage(editedContent, images)
 		} catch (error) {
 			console.error("Error in edit message:", error)
 			vscode.window.showErrorMessage(
-				`Error editing message: ${error instanceof Error ? error.message : String(error)}`,
+				t("common:errors.message.error_editing_message", {
+					error: error instanceof Error ? error.message : String(error),
+				}),
 			)
 		}
 	}
@@ -459,7 +509,7 @@ export const webviewMessageHandler = async (
 			// If user already opted in to telemetry, enable telemetry service
 			provider.getStateToPostToWebview().then(async (/*kilocode_change*/ state) => {
 				const { telemetrySetting } = state
-				const isOptedIn = telemetrySetting === "enabled"
+				const isOptedIn = telemetrySetting !== "disabled"
 				TelemetryService.instance.updateTelemetryState(isOptedIn)
 				await TelemetryService.instance.updateIdentity(state.apiConfiguration.kilocodeToken ?? "") // kilocode_change
 			})
@@ -837,7 +887,7 @@ export const webviewMessageHandler = async (
 					if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
 						provider.postMessageToWebview({
 							type: "ollamaModels",
-							ollamaModels: Object.keys(result.value.models),
+							ollamaModels: result.value.models,
 						})
 					} else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) {
 						provider.postMessageToWebview({
@@ -877,12 +927,13 @@ export const webviewMessageHandler = async (
 				const ollamaModels = await getModels({
 					provider: "ollama",
 					baseUrl: ollamaApiConfig.ollamaBaseUrl,
+					apiKey: ollamaApiConfig.ollamaApiKey,
 				})
 
 				if (Object.keys(ollamaModels).length > 0) {
 					provider.postMessageToWebview({
 						type: "ollamaModels",
-						ollamaModels: Object.keys(ollamaModels),
+						ollamaModels: ollamaModels,
 					})
 				}
 			} catch (error) {
@@ -1211,16 +1262,21 @@ 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 provider.remoteControlEnabled(message.bool ?? false)
+				await CloudService.instance.updateUserSettings(updatedSettings)
 			} catch (error) {
-				provider.log(
-					`ClineProvider#remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
-				)
+				provider.log(`Failed to update cloud settings for task sync: ${error}`)
 			}
-
-			await provider.postStateToWebview()
 			break
 		case "refreshAllMcpServers": {
 			const mcpHub = provider.getMcpHub()
@@ -1529,9 +1585,17 @@ export const webviewMessageHandler = async (
 			}
 			break
 		case "deleteMessage": {
-			if (provider.getCurrentTask() && typeof message.value === "number" && message.value) {
-				await handleMessageModificationsOperation(message.value, "delete")
+			if (!provider.getCurrentTask()) {
+				await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete"))
+				break
+			}
+
+			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": {
@@ -2017,9 +2081,17 @@ export const webviewMessageHandler = async (
 			}
 			break
 		case "deleteMessageConfirm":
-			if (message.messageTs) {
-				await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint)
+			if (!message.messageTs) {
+				await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp"))
+				break
+			}
+
+			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) {
@@ -2646,6 +2718,48 @@ 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) {
@@ -3378,5 +3492,39 @@ 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
+		}
 	}
 }

+ 7 - 34
src/extension.ts

@@ -170,11 +170,10 @@ export async function activate(context: vscode.ExtensionContext) {
 
 		if (data.state === "logged-out") {
 			try {
-				await BridgeOrchestrator.disconnect()
-				cloudLogger("[CloudService] BridgeOrchestrator disconnected on logout")
+				await provider.remoteControlEnabled(false)
 			} catch (error) {
 				cloudLogger(
-					`[CloudService] Failed to disconnect BridgeOrchestrator on logout: ${error instanceof Error ? error.message : String(error)}`,
+					`[authStateChangedHandler] remoteControlEnabled(false) failed: ${error instanceof Error ? error.message : String(error)}`,
 				)
 			}
 		}
@@ -185,23 +184,10 @@ export async function activate(context: vscode.ExtensionContext) {
 
 		if (userInfo && CloudService.instance.cloudAPI) {
 			try {
-				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,
-				})
+				provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
 			} catch (error) {
 				cloudLogger(
-					`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on settings change: ${error instanceof Error ? error.message : String(error)}`,
+					`[settingsUpdatedHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
 				)
 			}
 		}
@@ -213,28 +199,15 @@ export async function activate(context: vscode.ExtensionContext) {
 		postStateListener()
 
 		if (!CloudService.instance.cloudAPI) {
-			cloudLogger("[CloudService] CloudAPI is not initialized")
+			cloudLogger("[userInfoHandler] CloudAPI is not initialized")
 			return
 		}
 
 		try {
-			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,
-			})
+			provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled())
 		} catch (error) {
 			cloudLogger(
-				`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on user change: ${error instanceof Error ? error.message : String(error)}`,
+				`[userInfoHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`,
 			)
 		}
 	}

+ 22 - 2
src/i18n/locales/ca/common.json

@@ -41,6 +41,7 @@
 		"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'ha detectat un repositori git niat a: {{path}}. Per utilitzar punts de control, si us plau elimina o reubica aquest repositori git niat.",
 		"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",
@@ -108,6 +109,15 @@
 			"apiError": "Error de l'API Gemini CLI: {{error}}",
 			"completionError": "Error de compleció de Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -131,6 +141,11 @@
 		"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": {
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "La teva organització requereix autenticació de Kilo Code Cloud. Si us plau, inicia sessió per continuar.",
-			"organization_mismatch": "Has d'estar autenticat amb el compte de Kilo Code Cloud de la teva organització.",
+			"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": {

+ 16 - 1
src/i18n/locales/de/common.json

@@ -37,6 +37,7 @@
 		"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 ein verschachteltes Git-Repository erkannt wurde unter: {{path}}. Um Checkpoints zu verwenden, entferne oder verschiebe bitte dieses verschachtelte Git-Repository.",
 		"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",
@@ -105,6 +106,15 @@
 			"apiError": "Gemini CLI API-Fehler: {{error}}",
 			"completionError": "Gemini CLI Vervollständigungsfehler: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",

+ 16 - 1
src/i18n/locales/en/common.json

@@ -37,6 +37,7 @@
 		"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 a nested git repository was detected at: {{path}}. To use checkpoints, please remove or relocate this nested git repository.",
 		"no_workspace": "Please open a project folder first",
 		"update_support_prompt": "Failed to update support prompt",
 		"reset_support_prompt": "Failed to reset support prompt",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "Failed to load OAuth credentials. Please authenticate first: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",

+ 16 - 1
src/i18n/locales/es/common.json

@@ -37,6 +37,7 @@
 		"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 detectó un repositorio git anidado en: {{path}}. Para usar puntos de control, por favor elimina o reubica este repositorio git anidado.",
 		"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",
@@ -105,6 +106,15 @@
 			"apiError": "Error de API de Gemini CLI: {{error}}",
 			"completionError": "Error de completado de Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",

+ 23 - 3
src/i18n/locales/fr/common.json

@@ -37,6 +37,7 @@
 		"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 un dépôt git imbriqué a été détecté à : {{path}}. Pour utiliser les points de contrôle, veuillez supprimer ou déplacer ce dépôt git imbriqué.",
 		"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",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "Échec du chargement des identifiants OAuth. Veuillez vous authentifier d'abord : {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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é",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Votre organisation nécessite une authentification Kilo Code Cloud. Veuillez vous connecter pour continuer.",
-			"organization_mismatch": "Vous devez être authentifié avec le compte Kilo Code Cloud de votre organisation.",
+			"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": {

+ 23 - 3
src/i18n/locales/hi/common.json

@@ -37,6 +37,7 @@
 		"checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।",
 		"checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।",
 		"git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट সক্ষম करने के लिए Git इंस्टॉल करें।",
+		"nested_git_repos_warning": "चेकपॉइंट अक्षम हैं क्योंकि {{path}} पर नेस्टेड git रिपॉजिटरी का पता चला है। चेकपॉइंट का उपयोग करने के लिए, कृपया इस नेस्टेड git रिपॉजिटरी को हटाएं या स्थानांतरित करें।",
 		"no_workspace": "कृपया पहले प्रोजेक्ट फ़ोल्डर खोलें",
 		"update_support_prompt": "सपोर्ट प्रॉम्प्ट अपडेट करने में विफल",
 		"reset_support_prompt": "सपोर्ट प्रॉम्प्ट रीसेट करने में विफल",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "OAuth क्रेडेंशियल लोड करने में विफल। कृपया पहले प्रमाणीकरण करें: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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": "कोई टर्मिनल सामग्री चयनित नहीं",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "आपके संगठन को Kilo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",
-			"organization_mismatch": "आपको अपने संगठन के Kilo Code Cloud खाते से प्रमाणित होना होगा।",
+			"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": {

+ 23 - 3
src/i18n/locales/id/common.json

@@ -37,6 +37,7 @@
 		"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: {{path}}. Untuk menggunakan checkpoint, silakan hapus atau pindahkan repositori git bersarang ini.",
 		"no_workspace": "Silakan buka folder proyek terlebih dahulu",
 		"update_support_prompt": "Gagal memperbarui support prompt",
 		"reset_support_prompt": "Gagal mereset support prompt",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "Gagal memuat kredensial OAuth. Silakan autentikasi terlebih dahulu: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Kilo Code Cloud. Silakan masuk untuk melanjutkan.",
-			"organization_mismatch": "Kamu harus diautentikasi dengan akun Kilo Code Cloud organisasi kamu.",
+			"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": {

+ 23 - 3
src/i18n/locales/it/common.json

@@ -37,6 +37,7 @@
 		"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é è stato rilevato un repository git annidato in: {{path}}. Per utilizzare i checkpoint, rimuovi o sposta questo repository git annidato.",
 		"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",
@@ -105,6 +106,15 @@
 			"apiError": "Errore API Gemini CLI: {{error}}",
 			"completionError": "Errore di completamento Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "La tua organizzazione richiede l'autenticazione Kilo Code Cloud. Accedi per continuare.",
-			"organization_mismatch": "Devi essere autenticato con l'account Kilo Code Cloud della tua organizzazione.",
+			"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": {

+ 23 - 3
src/i18n/locales/ja/common.json

@@ -37,6 +37,7 @@
 		"checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。",
 		"checkpoint_failed": "チェックポイントの復元に失敗しました。",
 		"git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。",
+		"nested_git_repos_warning": "{{path}} でネストされたgitリポジトリが検出されたため、チェックポイントが無効になっています。チェックポイントを使用するには、このネストされたgitリポジトリを削除または移動してください。",
 		"no_workspace": "まずプロジェクトフォルダを開いてください",
 		"update_support_prompt": "サポートメッセージの更新に失敗しました",
 		"reset_support_prompt": "サポートメッセージのリセットに失敗しました",
@@ -105,6 +106,15 @@
 			"apiError": "Gemini CLI APIエラー: {{error}}",
 			"completionError": "Gemini CLI補完エラー: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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": "選択されたターミナルコンテンツがありません",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "あなたの組織では Kilo Code Cloud 認証が必要です。続行するにはサインインしてください。",
-			"organization_mismatch": "組織の Kilo Code Cloud アカウントで認証する必要があります。",
+			"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": {

+ 23 - 3
src/i18n/locales/ko/common.json

@@ -37,6 +37,7 @@
 		"checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.",
 		"checkpoint_failed": "체크포인트 복원에 실패했습니다.",
 		"git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.",
+		"nested_git_repos_warning": "{{path}}에서 중첩된 git 저장소가 감지되어 체크포인트가 비활성화되었습니다. 체크포인트를 사용하려면 이 중첩된 git 저장소를 제거하거나 이동해주세요.",
 		"no_workspace": "먼저 프로젝트 폴더를 열어주세요",
 		"update_support_prompt": "지원 프롬프트 업데이트에 실패했습니다",
 		"reset_support_prompt": "지원 프롬프트 재설정에 실패했습니다",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "OAuth 자격 증명을 로드하지 못했습니다. 먼저 인증하세요: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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": "선택된 터미널 내용이 없습니다",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "조직에서 Kilo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",
-			"organization_mismatch": "조직의 Kilo Code Cloud 계정으로 인증해야 합니다.",
+			"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": {

+ 23 - 3
src/i18n/locales/nl/common.json

@@ -37,6 +37,7 @@
 		"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 een geneste git-repository is gedetecteerd op: {{path}}. Om checkpoints te gebruiken, verwijder of verplaats deze geneste git-repository.",
 		"no_workspace": "Open eerst een projectmap",
 		"update_support_prompt": "Bijwerken van ondersteuningsprompt mislukt",
 		"reset_support_prompt": "Resetten van ondersteuningsprompt mislukt",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "Kan OAuth-referenties niet laden. Authenticeer eerst: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Je organisatie vereist Kilo Code Cloud-authenticatie. Log in om door te gaan.",
-			"organization_mismatch": "Je moet geauthenticeerd zijn met het Kilo Code Cloud-account van je organisatie.",
+			"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": {

+ 23 - 3
src/i18n/locales/pl/common.json

@@ -37,6 +37,7 @@
 		"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 repozytorium git w: {{path}}. Aby używać punktów kontrolnych, usuń lub przenieś to zagnieżdżone repozytorium 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",
@@ -105,6 +106,15 @@
 			"apiError": "Błąd API Gemini CLI: {{error}}",
 			"completionError": "Błąd uzupełniania Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Kilo Code Cloud. Zaloguj się, aby kontynuować.",
-			"organization_mismatch": "Musisz być uwierzytelniony kontem Kilo Code Cloud swojej organizacji.",
+			"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": {

+ 23 - 3
src/i18n/locales/pt-BR/common.json

@@ -41,6 +41,7 @@
 		"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 um repositório git aninhado foi detectado em: {{path}}. Para usar checkpoints, por favor remova ou realoque este repositório git aninhado.",
 		"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",
@@ -109,6 +110,15 @@
 			"apiError": "Erro da API Gemini CLI: {{error}}",
 			"completionError": "Erro de conclusão do Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -131,7 +141,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Sua organização requer autenticação do Kilo Code Cloud. Faça login para continuar.",
-			"organization_mismatch": "Você deve estar autenticado com a conta Kilo Code Cloud da sua organização.",
+			"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": {

+ 23 - 3
src/i18n/locales/ru/common.json

@@ -37,6 +37,7 @@
 		"checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.",
 		"checkpoint_failed": "Не удалось восстановить контрольную точку.",
 		"git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.",
+		"nested_git_repos_warning": "Контрольные точки отключены, поскольку обнаружен вложенный git-репозиторий в: {{path}}. Чтобы использовать контрольные точки, пожалуйста, удалите или переместите этот вложенный git-репозиторий.",
 		"no_workspace": "Пожалуйста, сначала откройте папку проекта",
 		"update_support_prompt": "Не удалось обновить промпт поддержки",
 		"reset_support_prompt": "Не удалось сбросить промпт поддержки",
@@ -105,6 +106,15 @@
 			"apiError": "Ошибка API Gemini CLI: {{error}}",
 			"completionError": "Ошибка завершения Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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": "Не выбрано содержимое терминала",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Ваша организация требует аутентификации Kilo Code Cloud. Войдите в систему, чтобы продолжить.",
-			"organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Kilo Code Cloud вашей организации.",
+			"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": {

+ 23 - 3
src/i18n/locales/tr/common.json

@@ -37,6 +37,7 @@
 		"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": "{{path}} konumunda iç içe git deposu tespit edildiği için kontrol noktaları devre dışı bırakıldı. Kontrol noktalarını kullanmak için lütfen bu iç içe git deposunu 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ı",
@@ -108,6 +109,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "OAuth kimlik bilgileri yüklenemedi. Lütfen önce kimlik doğrulaması yapın: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Kuruluşunuz Kilo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.",
-			"organization_mismatch": "Kuruluşunuzun Kilo Code Cloud hesabıyla kimlik doğrulaması yapmalısınız.",
+			"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": {

+ 23 - 3
src/i18n/locales/vi/common.json

@@ -37,6 +37,7 @@
 		"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 kho git lồng nhau tại: {{path}}. Để sử dụng điểm kiểm tra, vui lòng xóa hoặc di chuyển kho git lồng nhau này.",
 		"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ợ",
@@ -105,6 +106,15 @@
 			"apiError": "Lỗi API Gemini CLI: {{error}}",
 			"completionError": "Lỗi hoàn thành Gemini CLI: {{error}}"
 		},
+		"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}}",
@@ -127,7 +137,12 @@
 		},
 		"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",
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Kilo 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 Kilo Code Cloud của tổ chức.",
+			"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": {

+ 23 - 3
src/i18n/locales/zh-CN/common.json

@@ -42,6 +42,7 @@
 		"checkpoint_timeout": "尝试恢复检查点时超时。",
 		"checkpoint_failed": "恢复检查点失败。",
 		"git_not_installed": "存档点功能需要 Git。请安装 Git 以启用存档点。",
+		"nested_git_repos_warning": "存档点已禁用,因为在 {{path}} 检测到嵌套的 git 仓库。要使用存档点,请移除或重新定位此嵌套的 git 仓库。",
 		"no_workspace": "请先打开项目文件夹",
 		"update_support_prompt": "更新支持消息失败",
 		"reset_support_prompt": "重置支持消息失败",
@@ -110,6 +111,15 @@
 			"apiError": "Gemini CLI API 错误:{{error}}",
 			"completionError": "Gemini CLI 补全错误:{{error}}"
 		},
+		"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}}",
@@ -132,7 +142,12 @@
 		},
 		"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": "没有选择终端内容",
@@ -217,8 +232,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "您的组织需要 Kilo Code Cloud 身份验证。请登录以继续。",
-			"organization_mismatch": "您必须使用组织的 Kilo Code Cloud 账户进行身份验证。",
+			"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": {

+ 22 - 2
src/i18n/locales/zh-TW/common.json

@@ -37,6 +37,7 @@
 		"checkpoint_timeout": "嘗試恢復檢查點時超時。",
 		"checkpoint_failed": "恢復檢查點失敗。",
 		"git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。",
+		"nested_git_repos_warning": "存檔點已停用,因為在 {{path}} 偵測到巢狀的 git 儲存庫。要使用存檔點,請移除或重新配置此巢狀的 git 儲存庫。",
 		"no_workspace": "請先開啟專案資料夾",
 		"update_support_prompt": "更新支援訊息失敗",
 		"reset_support_prompt": "重設支援訊息失敗",
@@ -107,6 +108,15 @@
 		"qwenCode": {
 			"oauthLoadFailed": "無法載入 OAuth 憑證。請先進行驗證:{{error}}"
 		},
+		"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}}",
@@ -127,6 +137,11 @@
 		"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": {
@@ -212,8 +227,13 @@
 	},
 	"mdm": {
 		"errors": {
-			"cloud_auth_required": "您的組織需要 Kilo Code Cloud 身份驗證。請登入以繼續。",
-			"organization_mismatch": "您必須使用組織的 Kilo Code Cloud 帳戶進行身份驗證。",
+			"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": {

+ 42 - 17
src/services/checkpoints/ShadowCheckpointService.ts

@@ -6,9 +6,11 @@ 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"
@@ -17,9 +19,6 @@ import { getExcludePatterns } from "./excludes"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryEventName } from "@roo-code/types"
 import { stringifyError } from "../../shared/kilocode/errorUtils"
-import * as vscode from "vscode"
-import { t } from "../../i18n"
-
 function reportError(callsite: string, error: unknown) {
 	TelemetryService.instance.captureEvent(TelemetryEventName.CHECKPOINT_FAILURE, {
 		callsite,
@@ -92,12 +91,18 @@ export abstract class ShadowCheckpointService extends EventEmitter {
 			throw new Error("Shadow git repo already initialized")
 		}
 
-		const hasNestedGitRepos = await this.hasNestedGitRepositories()
+		const nestedGitPath = await this.getNestedGitRepository()
+
+		if (nestedGitPath) {
+			// Show persistent error message with the offending path
+			const relativePath = path.relative(this.workspaceDir, nestedGitPath)
+			const message = t("common:errors.nested_git_repos_warning", { path: relativePath })
+			vscode.window.showErrorMessage(message)
 
-		if (hasNestedGitRepos) {
 			showWarning(t("kilocode:checkpoints.nestedGitRepos")) // kilocode_change
+
 			throw new Error(
-				"Checkpoints are disabled because nested git repositories were detected in the workspace. " +
+				`Checkpoints are disabled because a nested git repository was detected at: ${relativePath}. ` +
 					"Please remove or relocate nested git repositories to use the checkpoints feature.",
 			)
 		}
@@ -179,35 +184,55 @@ export abstract class ShadowCheckpointService extends EventEmitter {
 		}
 	}
 
-	private async hasNestedGitRepositories(): Promise<boolean> {
+	private async getNestedGitRepository(): Promise<string | null> {
 		try {
-			// Find all .git directories that are not at the root level.
+			// Find all .git/HEAD files that are not at the root level.
 			const args = ["--files", "--hidden", "--follow", "-g", "**/.git/HEAD", this.workspaceDir]
 
 			const gitPaths = await executeRipgrep({ args, workspacePath: this.workspaceDir })
 
 			// Filter to only include nested git directories (not the root .git).
-			const nestedGitPaths = gitPaths.filter(
-				({ type, path }) =>
-					type === "folder" && path.includes(".git") && !path.startsWith(".git") && path !== ".git",
-			)
+			// Since we're searching for HEAD files, we expect type to be "file"
+			const nestedGitPaths = gitPaths.filter(({ type, path: filePath }) => {
+				// Check if it's a file and is a nested .git/HEAD (not at root)
+				if (type !== "file") return false
+
+				// Ensure it's a .git/HEAD file and not the root one
+				const normalizedPath = filePath.replace(/\\/g, "/")
+				return (
+					normalizedPath.includes(".git/HEAD") &&
+					!normalizedPath.startsWith(".git/") &&
+					normalizedPath !== ".git/HEAD"
+				)
+			})
 
 			if (nestedGitPaths.length > 0) {
+				// Get the first nested git repository path
+				// Remove .git/HEAD from the path to get the repository directory
+				const headPath = nestedGitPaths[0].path
+
+				// Use path module to properly extract the repository directory
+				// The HEAD file is at .git/HEAD, so we need to go up two directories
+				const gitDir = path.dirname(headPath) // removes HEAD, gives us .git
+				const repoDir = path.dirname(gitDir) // removes .git, gives us the repo directory
+
+				const absolutePath = path.join(this.workspaceDir, repoDir)
+
 				this.log(
-					`[${this.constructor.name}#hasNestedGitRepositories] found ${nestedGitPaths.length} nested git repositories: ${nestedGitPaths.map((p) => p.path).join(", ")}`,
+					`[${this.constructor.name}#getNestedGitRepository] found ${nestedGitPaths.length} nested git repositories, first at: ${repoDir}`,
 				)
-				return true
+				return absolutePath
 			}
 
-			return false
+			return null
 		} catch (error) {
 			this.log(
-				`[${this.constructor.name}#hasNestedGitRepositories] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
+				`[${this.constructor.name}#getNestedGitRepository] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
 			)
 			reportError(`${this.constructor.name}#hasNestedGitRepositories`, error) // kilocode_change
 
 			// If we can't check, assume there are no nested repos to avoid blocking the feature.
-			return false
+			return null
 		}
 	}
 

+ 8 - 5
src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts

@@ -431,11 +431,13 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
 					const searchPattern = args[4]
 
 					if (searchPattern.includes(".git/HEAD")) {
+						// Return the HEAD file path, not the .git directory
+						const headFilePath = path.join(path.relative(workspaceDir, nestedGitDir), "HEAD")
 						return Promise.resolve([
 							{
-								path: path.relative(workspaceDir, nestedGitDir),
-								type: "folder",
-								label: ".git",
+								path: headFilePath,
+								type: "file", // HEAD is a file, not a folder
+								label: "HEAD",
 							},
 						])
 					} else {
@@ -446,8 +448,9 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
 				const service = new klass(taskId, shadowDir, workspaceDir, () => {})
 
 				// Verify that initialization throws an error when nested git repos are detected
-				await expect(service.initShadowGit()).rejects.toThrow(
-					"Checkpoints are disabled because nested git repositories were detected in the workspace",
+				// The error message now includes the specific path of the nested repository
+				await expect(service.initShadowGit()).rejects.toThrowError(
+					/Checkpoints are disabled because a nested git repository was detected at:/,
 				)
 
 				// Clean up.

+ 7 - 2
src/shared/ExtensionMessage.ts

@@ -140,6 +140,7 @@ export interface ExtensionMessage {
 		| "usageDataResponse" // kilocode_change
 		| "commands"
 		| "insertTextIntoTextarea"
+		| "dismissedUpsells"
 	text?: string
 	payload?: ProfileDataResponsePayload | BalanceDataResponsePayload // kilocode_change: Add payload for profile and balance data
 	action?:
@@ -167,7 +168,7 @@ export interface ExtensionMessage {
 	clineMessage?: ClineMessage
 	routerModels?: RouterModels
 	openAiModels?: string[]
-	ollamaModels?: string[]
+	ollamaModels?: ModelRecord
 	lmStudioModels?: ModelRecord
 	vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
 	huggingFaceModels?: Array<{
@@ -243,6 +244,7 @@ export interface ExtensionMessage {
 	// kilocode_change end
 	commands?: Command[]
 	queuedMessages?: QueuedMessage[]
+	list?: string[] // For dismissedUpsells
 }
 
 export type ExtensionState = Pick<
@@ -253,6 +255,7 @@ export type ExtensionState = Pick<
 	// | "lastShownAnnouncementId"
 	| "customInstructions"
 	// | "taskHistory" // Optional in GlobalSettings, required here.
+	| "dismissedUpsells"
 	| "autoApprovalEnabled"
 	| "alwaysAllowReadOnly"
 	| "alwaysAllowReadOnlyOutsideWorkspace"
@@ -334,7 +337,6 @@ export type ExtensionState = Pick<
 	| "systemNotificationsEnabled" // kilocode_change
 	| "includeDiagnosticMessages"
 	| "maxDiagnosticMessages"
-	| "remoteControlEnabled"
 	| "openRouterImageGenerationSelectedModel"
 	| "includeTaskHistoryInEnhance"
 > & {
@@ -406,6 +408,9 @@ export type ExtensionState = Pick<
 	mcpServers?: McpServer[]
 	hasSystemPromptOverride?: boolean
 	mdmCompliant?: boolean
+	remoteControlEnabled: boolean
+	taskSyncEnabled: boolean
+	featureRoomoteControlEnabled: boolean
 }
 
 export interface ClineSayTool {

+ 6 - 0
src/shared/WebviewMessage.ts

@@ -146,6 +146,7 @@ export interface WebviewMessage {
 		| "mcpEnabled"
 		| "enableMcpServerCreation"
 		| "remoteControlEnabled"
+		| "taskSyncEnabled"
 		| "searchCommits"
 		| "alwaysApproveResubmit"
 		| "requestDelaySeconds"
@@ -219,6 +220,7 @@ export interface WebviewMessage {
 		| "cloudButtonClicked"
 		| "rooCloudSignIn"
 		| "rooCloudSignOut"
+		| "rooCloudManualUrl"
 		| "condenseTaskContextRequest"
 		| "requestIndexingStatus"
 		| "startIndexing"
@@ -274,6 +276,8 @@ export interface WebviewMessage {
 		| "queueMessage"
 		| "removeQueuedMessage"
 		| "editQueuedMessage"
+		| "dismissUpsell"
+		| "getDismissedUpsells"
 	text?: string
 	editedMessageContent?: string
 	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -336,6 +340,8 @@ 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

+ 72 - 9
src/shared/__tests__/context-mentions.spec.ts

@@ -3,37 +3,55 @@ import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
 describe("mentionRegex and mentionRegexGlobal", () => {
 	// Test cases for various mention types
 	const testCases = [
-		// Basic file paths
+		// Basic file paths at line start
 		{ 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
+		// File paths with escaped spaces at line start
 		{ 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
+		// URLs at line start
 		{ input: "@http://example.com", expected: ["@http://example.com"] },
 		{ input: "@https://example.com/path?query=1", expected: ["@https://example.com/path?query=1"] },
 
-		// Other mentions
+		// Other mentions at line start
 		{ 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 within text
+		// Mentions after whitespace (valid)
 		{
 			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"] }, // Trailing punctuation
+		{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // After colon and space
 		{ 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 @
@@ -41,14 +59,15 @@ 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"] }, // 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
+		{ 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
 	]
 	testCases.forEach(({ input, expected }) => {
 		it(`should handle input: "${input}"`, () => {
@@ -83,4 +102,48 @@ 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)
+		})
+	})
 })

+ 1 - 1
src/shared/api.ts

@@ -154,7 +154,7 @@ export type GetModelsOptions =
 	| { provider: "litellm"; apiKey: string; baseUrl: string }
 	| { provider: "kilocode-openrouter"; kilocodeToken?: string; kilocodeOrganizationId?: string } // kilocode_change
 	| { provider: "cerebras"; cerebrasApiKey?: string } // kilocode_change
-	| { provider: "ollama"; baseUrl?: string }
+	| { provider: "ollama"; baseUrl?: string; apiKey?: string }
 	| { provider: "lmstudio"; baseUrl?: string }
 	| { provider: "deepinfra"; apiKey?: string; baseUrl?: string }
 	| { provider: "io-intelligence"; apiKey: string }

+ 16 - 9
src/shared/context-mentions.ts

@@ -1,19 +1,25 @@
 /*
 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+:\/\/`:
@@ -25,7 +31,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.
@@ -34,9 +40,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**:
@@ -48,13 +54,14 @@ 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 =
-	/(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
+	/(?:^|(?<=\s))(?<!\\)@((?:\/|\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

+ 2 - 0
webview-ui/package.json

@@ -34,6 +34,7 @@
 		"@tanstack/react-query": "^5.68.0",
 		"@types/seedrandom": "^3.0.8",
 		"@use-gesture/react": "^10.3.1",
+		"@types/qrcode": "^1.5.5",
 		"@vscode/codicons": "^0.0.36",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
 		"axios": "^1.7.4",
@@ -55,6 +56,7 @@
 		"mermaid": "^11.4.1",
 		"posthog-js": "^1.227.2",
 		"pretty-bytes": "^7.0.0",
+		"qrcode": "^1.5.4",
 		"react": "^18.3.1",
 		"react-countup": "^6.5.3",
 		"react-dom": "^18.3.1",

+ 2 - 2
webview-ui/src/App.tsx

@@ -209,11 +209,11 @@ const App = () => {
 	useEvent("message", onMessage)
 
 	useEffect(() => {
-		if (shouldShowAnnouncement) {
+		if (shouldShowAnnouncement && tab === "chat") {
 			setShowAnnouncement(true)
 			vscode.postMessage({ type: "didShowAnnouncement" })
 		}
-	}, [shouldShowAnnouncement])
+	}, [shouldShowAnnouncement, tab])
 
 	// kilocode_change start
 	const telemetryDistinctId = useKiloIdentity(apiConfiguration?.kilocodeToken ?? "", machineId ?? "")

+ 105 - 75
webview-ui/src/components/chat/Announcement.tsx

@@ -6,9 +6,12 @@ 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 } from "@src/components/ui"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } 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
 }
@@ -25,7 +28,8 @@ interface AnnouncementProps {
 const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
 	const { t } = useAppTranslation()
 	const [open, setOpen] = useState(true)
-	const { cloudIsAuthenticated } = useExtensionState()
+	const { cloudApiUrl } = useExtensionState()
+	const cloudUrl = cloudApiUrl || PRODUCTION_ROO_CODE_API_URL
 
 	return (
 		<Dialog
@@ -40,95 +44,82 @@ 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>
-					<div className="space-y-2">
-						<div>
+					<ul className="space-y-2">
+						<li>
+							•{" "}
 							<Trans
-								i18nKey="chat:announcement.stealthModel.feature"
+								i18nKey="chat:announcement.feature1"
 								components={{
 									bold: <b />,
-									code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
 								}}
 							/>
-						</div>
-					</div>
+						</li>
+						<li>
+							•{" "}
+							<Trans
+								i18nKey="chat:announcement.feature2"
+								components={{
+									bold: <b />,
+								}}
+							/>
+						</li>
+					</ul>
 
 					<div className="mt-4">
 						<Trans
-							i18nKey="chat:announcement.stealthModel.note"
+							i18nKey="chat:announcement.learnMore"
 							components={{
-								bold: <b />,
-								code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
+								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",
+													},
+												},
+												"*",
+											)
+										}}
+									/>
+								),
 							}}
 						/>
 					</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>
-								<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>
-						)}
+						<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>
 				</div>
 			</DialogContent>
@@ -136,5 +127,44 @@ 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)
 // kilocode_change: file unused, no need to touch anything

+ 1 - 8
webview-ui/src/components/chat/ApiConfigSelector.tsx

@@ -1,6 +1,5 @@
 import { useState, useMemo, useCallback } from "react"
 import { Fzf } from "fzf"
-import { ChevronUp } from "lucide-react"
 
 import { cn } from "@/lib/utils"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
@@ -149,7 +148,7 @@ export const ApiConfigSelector = ({
 					disabled={disabled}
 					data-testid="dropdown-trigger"
 					className={cn(
-						"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
+						"min-w-0 inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs",
 						"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
 						"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
 						disabled
@@ -157,12 +156,6 @@ export const ApiConfigSelector = ({
 							: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
 						triggerClassName,
 					)}>
-					<ChevronUp
-						className={cn(
-							"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
-							open && "rotate-180",
-						)}
-					/>
 					<span className="truncate">{displayName}</span>
 				</PopoverTrigger>
 			</StandardTooltip>

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