Просмотр исходного кода

Merge pull request #2623 from Kilo-Org/roo-v3.28.7

Include changes from Roo Code v3.28.7
Kevin van Dijk 4 месяцев назад
Родитель
Сommit
61d5211b7e
100 измененных файлов с 3946 добавлено и 553 удалено
  1. 27 0
      .changeset/brave-plants-camp.md
  2. 0 16
      .github/ISSUE_TEMPLATE/bug_report.yml
  3. 0 0
      apps/web-docs/.gitkeep
  4. 0 1
      apps/web-docs/README.md
  5. 1 1
      apps/web-evals/package.json
  6. 2 0
      apps/web-roo-code/package.json
  7. 2 11
      apps/web-roo-code/src/app/layout.tsx
  8. 198 0
      apps/web-roo-code/src/app/legal/cookies/page.tsx
  9. 183 0
      apps/web-roo-code/src/app/legal/subprocessors/page.tsx
  10. 7 4
      apps/web-roo-code/src/app/page.tsx
  11. 14 1
      apps/web-roo-code/src/app/privacy/page.tsx
  12. 111 0
      apps/web-roo-code/src/components/CookieConsentWrapper.tsx
  13. 14 0
      apps/web-roo-code/src/components/chromes/footer.tsx
  14. 92 0
      apps/web-roo-code/src/components/providers/google-analytics-provider.tsx
  15. 42 20
      apps/web-roo-code/src/components/providers/posthog-provider.tsx
  16. 8 5
      apps/web-roo-code/src/components/providers/providers.tsx
  17. 47 0
      apps/web-roo-code/src/lib/analytics/consent-manager.ts
  18. 1 1
      packages/build/src/__tests__/index.test.ts
  19. 12 3
      packages/build/src/esbuild.ts
  20. 14 0
      packages/build/src/types.ts
  21. 112 3
      packages/cloud/src/CloudService.ts
  22. 8 0
      packages/cloud/src/StaticTokenAuthService.ts
  23. 57 24
      packages/cloud/src/TelemetryClient.ts
  24. 57 6
      packages/cloud/src/WebAuthService.ts
  25. 0 22
      packages/cloud/src/__tests__/TelemetryClient.test.ts
  26. 3 0
      packages/cloud/src/index.ts
  27. 371 0
      packages/cloud/src/retry-queue/RetryQueue.ts
  28. 698 0
      packages/cloud/src/retry-queue/__tests__/RetryQueue.test.ts
  29. 2 0
      packages/cloud/src/retry-queue/index.ts
  30. 36 0
      packages/cloud/src/retry-queue/types.ts
  31. 1 1
      packages/evals/README.md
  32. 1 1
      packages/evals/docker-compose.yml
  33. 2 2
      packages/evals/scripts/setup.sh
  34. 1 1
      packages/types/npm/package.metadata.json
  35. 173 0
      packages/types/src/__tests__/cloud.test.ts
  36. 16 1
      packages/types/src/cloud.ts
  37. 22 0
      packages/types/src/cookie-consent.ts
  38. 1 0
      packages/types/src/global-settings.ts
  39. 1 0
      packages/types/src/index.ts
  40. 1 0
      packages/types/src/message.ts
  41. 174 61
      packages/types/src/provider-settings.ts
  42. 5 5
      packages/types/src/providers/chutes.ts
  43. 11 2
      packages/types/src/providers/roo.ts
  44. 1 3
      packages/types/src/providers/zai.ts
  45. 1 1
      packages/types/src/single-file-read-models.ts
  46. 1 0
      packages/types/src/vscode.ts
  47. 25 8
      pnpm-lock.yaml
  48. BIN
      releases/3.28.3-release.png
  49. BIN
      releases/3.28.4-release.png
  50. BIN
      releases/3.28.5-release.png
  51. BIN
      releases/3.28.6-release.png
  52. BIN
      releases/3.28.7-release.png
  53. 217 64
      scripts/find-missing-translations.js
  54. 12 0
      src/activate/registerCommands.ts
  55. 2 0
      src/api/index.ts
  56. 22 0
      src/api/providers/__tests__/chutes.spec.ts
  57. 98 0
      src/api/providers/__tests__/native-ollama.spec.ts
  58. 20 25
      src/api/providers/__tests__/roo.spec.ts
  59. 1 1
      src/api/providers/__tests__/zai.spec.ts
  60. 22 58
      src/api/providers/fetchers/huggingface.ts
  61. 6 34
      src/api/providers/fetchers/io-intelligence.ts
  62. 9 9
      src/api/providers/fetchers/lmstudio.ts
  63. 20 17
      src/api/providers/fetchers/modelCache.ts
  64. 10 2
      src/api/providers/fetchers/ollama.ts
  65. 11 4
      src/api/providers/gemini.ts
  66. 3 1
      src/api/providers/io-intelligence.ts
  67. 40 11
      src/api/providers/native-ollama.ts
  68. 36 6
      src/api/providers/roo.ts
  69. 3 3
      src/api/providers/zai.ts
  70. 35 31
      src/core/prompts/__tests__/custom-system-prompt.spec.ts
  71. 35 31
      src/core/task/Task.ts
  72. 243 0
      src/core/tools/__tests__/updateTodoListTool.spec.ts
  73. 2 1
      src/core/tools/updateTodoListTool.ts
  74. 76 8
      src/core/webview/ClineProvider.ts
  75. 6 0
      src/core/webview/__tests__/ClineProvider.spec.ts
  76. 10 0
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  77. 98 42
      src/core/webview/webviewMessageHandler.ts
  78. 1 1
      src/extension.ts
  79. 16 0
      src/i18n/locales/ar/common.json
  80. 16 0
      src/i18n/locales/ca/common.json
  81. 16 0
      src/i18n/locales/cs/common.json
  82. 16 0
      src/i18n/locales/de/common.json
  83. 16 0
      src/i18n/locales/en/common.json
  84. 16 0
      src/i18n/locales/es/common.json
  85. 16 0
      src/i18n/locales/fr/common.json
  86. 16 0
      src/i18n/locales/hi/common.json
  87. 16 0
      src/i18n/locales/id/common.json
  88. 16 0
      src/i18n/locales/it/common.json
  89. 16 0
      src/i18n/locales/ja/common.json
  90. 16 0
      src/i18n/locales/ko/common.json
  91. 16 0
      src/i18n/locales/nl/common.json
  92. 16 0
      src/i18n/locales/pl/common.json
  93. 16 0
      src/i18n/locales/pt-BR/common.json
  94. 16 0
      src/i18n/locales/ru/common.json
  95. 16 0
      src/i18n/locales/th/common.json
  96. 16 0
      src/i18n/locales/tr/common.json
  97. 16 0
      src/i18n/locales/uk/common.json
  98. 16 0
      src/i18n/locales/vi/common.json
  99. 16 0
      src/i18n/locales/zh-CN/common.json
  100. 16 0
      src/i18n/locales/zh-TW/common.json

+ 27 - 0
.changeset/brave-plants-camp.md

@@ -0,0 +1,27 @@
+---
+"kilo-code": minor
+---
+
+Include changes from Roo Code v3.28.2-v3.28.7
+
+- UX: Collapse thinking blocks by default with UI settings to always show them (thanks @brunobergher!)
+- Fix: Resolve checkpoint restore popover positioning issue (#8219 by @NaccOll, PR by @app/roomote)
+- Add support for zai-org/GLM-4.5-turbo model in Chutes provider (#8155 by @mugnimaestra, PR by @app/roomote)
+- Fix: Improve reasoning block formatting for better readability (thanks @daniel-lxs!)
+- Fix: Respect Ollama Modelfile num_ctx configuration (#7797 by @hannesrudolph, PR by @app/roomote)
+- Fix: Prevent checkpoint text from wrapping in non-English languages (#8206 by @NaccOll, PR by @app/roomote)
+- Fix: Bare metal evals fixes (thanks @cte!)
+- Fix: Follow-up questions should trigger the "interactive" state (thanks @cte!)
+- Fix: Resolve duplicate rehydrate during reasoning; centralize rehydrate and preserve cancel metadata (#8153 by @hannesrudolph, PR by @hannesrudolph)
+- Fix: Support dash prefix in parseMarkdownChecklist for todo lists (#8054 by @NaccOll, PR by app/roomote)
+- Fix: Apply tiered pricing for Gemini models via Vertex AI (#8017 by @ikumi3, PR by app/roomote)
+- Update SambaNova models to latest versions (thanks @snova-jorgep!)
+- UX: Redesigned Message Feed (thanks @brunobergher!)
+- UX: Responsive Auto-Approve (thanks @brunobergher!)
+- Add telemetry retry queue for network resilience (thanks @daniel-lxs!)
+- Fix: Filter out Claude Code built-in tools (ExitPlanMode, BashOutput, KillBash) (#7817 by @juliettefournier-econ, PR by @roomote)
+- Fix: Corrected C# tree-sitter query (#5238 by @vadash, PR by @mubeen-zulfiqar)
+- Add keyboard shortcut for "Add to Context" action (#7907 by @hannesrudolph, PR by @roomote)
+- Fix: Context menu is obscured when edit message (#7759 by @mini2s, PR by @NaccOll)
+- Fix: Handle ByteString conversion errors in OpenAI embedders (#7959 by @PavelA85, PR by @daniel-lxs)
+- Bring back a way to temporarily and globally pause auto-approve without losing your toggle state (thanks @brunobergher!)

+ 0 - 16
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -1,16 +0,0 @@
-name: Bug Report
-description: File a bug report
-type: "Bug"
-body:
-    - type: textarea
-      id: description
-      attributes:
-          label: Description
-          description: |
-            Which app version are you using (e.g. v3.3.1)?
-            Which API Provider are you using (e.g. Kilo, OpenRouter, Anthropic)?
-            Which Model are you using (e.g. Claude 3.7 Sonnet)?
-            Steps to reproduce?
-            Relevant API request output?
-            Additional context?
-            Screenshots?

+ 0 - 0
apps/web-docs/.gitkeep


+ 0 - 1
apps/web-docs/README.md

@@ -1 +0,0 @@
-TODO

+ 1 - 1
apps/web-evals/package.json

@@ -5,7 +5,7 @@
 	"scripts": {
 		"lint": "next lint --max-warnings 0",
 		"check-types": "tsc -b",
-		"dev": "scripts/check-services.sh && next dev",
+		"dev": "scripts/check-services.sh && next dev -p 3446",
 		"format": "prettier --write src",
 		"build": "next build",
 		"start": "next start",

+ 2 - 0
apps/web-roo-code/package.json

@@ -28,11 +28,13 @@
 		"next-themes": "^0.4.6",
 		"posthog-js": "^1.248.1",
 		"react": "^18.3.1",
+		"react-cookie-consent": "^9.0.0",
 		"react-dom": "^18.3.1",
 		"react-icons": "^5.5.0",
 		"recharts": "^2.15.3",
 		"tailwind-merge": "^3.3.0",
 		"tailwindcss-animate": "^1.0.7",
+		"tldts": "^6.1.86",
 		"zod": "^3.25.61"
 	},
 	"devDependencies": {

+ 2 - 11
apps/web-roo-code/src/app/layout.tsx

@@ -1,8 +1,8 @@
 import React from "react"
 import type { Metadata } from "next"
 import { Inter } from "next/font/google"
-import Script from "next/script"
 import { SEO } from "@/lib/seo"
+import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"
 
 import { Providers } from "@/components/providers"
 
@@ -93,22 +93,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
 				/>
 			</head>
 			<body className={inter.className}>
-				{/* Google tag (gtag.js) */}
-				<Script src="https://www.googletagmanager.com/gtag/js?id=AW-17391954825" strategy="afterInteractive" />
-				<Script id="google-analytics" strategy="afterInteractive">
-					{`
-						window.dataLayer = window.dataLayer || [];
-						function gtag(){dataLayer.push(arguments);}
-						gtag('js', new Date());
-						gtag('config', 'AW-17391954825');
-					`}
-				</Script>
 				<div itemScope itemType="https://schema.org/WebSite">
 					<link itemProp="url" href={SEO.url} />
 					<meta itemProp="name" content={SEO.name} />
 				</div>
 				<Providers>
 					<Shell>{children}</Shell>
+					<CookieConsentWrapper />
 				</Providers>
 			</body>
 		</html>

+ 198 - 0
apps/web-roo-code/src/app/legal/cookies/page.tsx

@@ -0,0 +1,198 @@
+import type { Metadata } from "next"
+import { SEO } from "@/lib/seo"
+
+const TITLE = "Cookie Policy"
+const DESCRIPTION = "Learn about how Roo Code uses cookies to enhance your experience and provide our services."
+const PATH = "/legal/cookies"
+const OG_IMAGE = SEO.ogImage
+
+export const metadata: Metadata = {
+	title: TITLE,
+	description: DESCRIPTION,
+	alternates: {
+		canonical: `${SEO.url}${PATH}`,
+	},
+	openGraph: {
+		title: TITLE,
+		description: DESCRIPTION,
+		url: `${SEO.url}${PATH}`,
+		siteName: SEO.name,
+		images: [
+			{
+				url: OG_IMAGE.url,
+				width: OG_IMAGE.width,
+				height: OG_IMAGE.height,
+				alt: OG_IMAGE.alt,
+			},
+		],
+		locale: SEO.locale,
+		type: "article",
+	},
+	twitter: {
+		card: SEO.twitterCard,
+		title: TITLE,
+		description: DESCRIPTION,
+		images: [OG_IMAGE.url],
+	},
+	keywords: [...SEO.keywords, "cookies", "privacy", "tracking", "analytics"],
+}
+
+export default function CookiePolicy() {
+	return (
+		<>
+			<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
+				<div className="prose prose-lg mx-auto max-w-4xl dark:prose-invert">
+					<p className="text-muted-foreground">Updated: September 18, 2025</p>
+
+					<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">Cookie Policy</h1>
+
+					<p className="lead">
+						This Cookie Policy explains how Roo Code uses cookies and similar technologies to recognize you
+						when you visit our website.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">What are cookies?</h2>
+					<p>
+						Cookies are small data files that are placed on your computer or mobile device when you visit a
+						website. Cookies help make websites work more efficiently and provide reporting information.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Cookies we use</h2>
+					<p>
+						We use a minimal number of cookies to provide essential functionality and improve your
+						experience.
+					</p>
+
+					<div className="overflow-x-auto">
+						<table className="min-w-full border-collapse border border-border">
+							<thead>
+								<tr className="bg-muted/50">
+									<th className="border border-border px-4 py-3 text-left font-semibold">Provider</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">Purpose</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">Type</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">Duration</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">
+										Example Cookies
+									</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Clerk</td>
+									<td className="border border-border px-4 py-3">
+										Authentication and session management
+									</td>
+									<td className="border border-border px-4 py-3">Essential</td>
+									<td className="border border-border px-4 py-3">1 year and 1 month</td>
+									<td className="border border-border px-4 py-3 font-mono text-sm">
+										__client_uat*, __clerk_*
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">PostHog</td>
+									<td className="border border-border px-4 py-3">
+										Product analytics and feature usage tracking
+									</td>
+									<td className="border border-border px-4 py-3">
+										Analytics (only with your consent)
+									</td>
+									<td className="border border-border px-4 py-3">1 year</td>
+									<td className="border border-border px-4 py-3 font-mono text-sm">ph_*</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+
+					<p className="mt-4">
+						<a
+							href="https://clerk.com/legal/privacy"
+							target="_blank"
+							rel="noopener noreferrer"
+							className="text-primary hover:underline">
+							Clerk Privacy Policy
+						</a>
+					</p>
+					<p>
+						<a
+							href="https://posthog.com/privacy"
+							target="_blank"
+							rel="noopener noreferrer"
+							className="text-primary hover:underline">
+							PostHog Privacy Policy
+						</a>
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Essential cookies</h2>
+					<p>
+						Essential cookies are required for our website to operate. These include authentication cookies
+						from Clerk that allow you to stay logged in to your account. These cookies cannot be disabled
+						without losing core website functionality. The lawful basis for processing these cookies is our
+						legitimate interest in providing secure access to our services.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Analytics cookies</h2>
+					<p>
+						We use PostHog analytics cookies to understand how visitors interact with our website. This
+						helps us improve our services and user experience. Analytics cookies are placed only if you give
+						consent through our cookie banner. The lawful basis for processing these cookies is your
+						consent, which you can withdraw at any time.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Third-party services</h2>
+					<p>
+						Our blog at{" "}
+						<a
+							href="https://blog.roocode.com"
+							target="_blank"
+							rel="noopener noreferrer"
+							className="text-primary hover:underline">
+							blog.roocode.com
+						</a>{" "}
+						is hosted on Substack. When you visit it, Substack may set cookies for analytics,
+						personalization, and advertising/marketing. These cookies are managed directly by Substack and
+						are outside our control. You can read more in{" "}
+						<a
+							href="https://substack.com/privacy"
+							target="_blank"
+							rel="noopener noreferrer"
+							className="text-primary hover:underline">
+							Substack&apos;s Cookie Policy
+						</a>
+						.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">How to control cookies</h2>
+					<p>You can control and manage cookies through your browser settings. Most browsers allow you to:</p>
+					<ul>
+						<li>View what cookies are stored on your device</li>
+						<li>Delete cookies individually or all at once</li>
+						<li>Block third-party cookies</li>
+						<li>Block cookies from specific websites</li>
+						<li>Block all cookies from being set</li>
+						<li>Delete all cookies when you close your browser</li>
+					</ul>
+					<p>
+						Please note that blocking essential cookies may prevent you from using certain features of our
+						website, such as staying logged in to your account.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Changes to this policy</h2>
+					<p>
+						We may update this Cookie Policy from time to time. When we make changes, we will update the
+						date at the top of this policy. We encourage you to periodically review this policy to stay
+						informed about our use of cookies.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">Contact us</h2>
+					<p>
+						If you have questions about our use of cookies, please contact us at{" "}
+						<a href="mailto:[email protected]" className="text-primary hover:underline">
+							[email protected]
+						</a>
+						.
+					</p>
+				</div>
+			</div>
+		</>
+	)
+}

+ 183 - 0
apps/web-roo-code/src/app/legal/subprocessors/page.tsx

@@ -0,0 +1,183 @@
+import type { Metadata } from "next"
+import { SEO } from "@/lib/seo"
+
+const TITLE = "Subprocessors"
+const DESCRIPTION = "List of third-party subprocessors used by Roo Code to process customer data."
+const PATH = "/legal/subprocessors"
+const OG_IMAGE = SEO.ogImage
+
+export const metadata: Metadata = {
+	title: TITLE,
+	description: DESCRIPTION,
+	alternates: {
+		canonical: `${SEO.url}${PATH}`,
+	},
+	openGraph: {
+		title: TITLE,
+		description: DESCRIPTION,
+		url: `${SEO.url}${PATH}`,
+		siteName: SEO.name,
+		images: [
+			{
+				url: OG_IMAGE.url,
+				width: OG_IMAGE.width,
+				height: OG_IMAGE.height,
+				alt: OG_IMAGE.alt,
+			},
+		],
+		locale: SEO.locale,
+		type: "article",
+	},
+	twitter: {
+		card: SEO.twitterCard,
+		title: TITLE,
+		description: DESCRIPTION,
+		images: [OG_IMAGE.url],
+	},
+	keywords: [...SEO.keywords, "subprocessors", "data processing", "GDPR", "privacy", "third-party services"],
+}
+
+export default function SubProcessors() {
+	return (
+		<>
+			<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
+				<div className="prose prose-lg mx-auto max-w-5xl dark:prose-invert">
+					<p className="text-muted-foreground">Updated: September 18, 2025</p>
+
+					<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">Subprocessors</h1>
+
+					<p className="lead">Roo Code engages the following third parties to process Customer Data.</p>
+
+					<div className="overflow-x-auto">
+						<table className="min-w-full border-collapse border border-border">
+							<thead>
+								<tr className="bg-muted/50">
+									<th className="border border-border px-4 py-3 text-left font-semibold">
+										Entity Name
+									</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">
+										Product or Service
+									</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">
+										Location of Processing
+									</th>
+									<th className="border border-border px-4 py-3 text-left font-semibold">
+										Purpose of Processing
+									</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Census</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Data activation and reverse ETL</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Clerk</td>
+									<td className="border border-border px-4 py-3">Authentication Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">User authentication</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">ClickHouse</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Real-time analytics database</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Cloudflare</td>
+									<td className="border border-border px-4 py-3">All Services</td>
+									<td className="border border-border px-4 py-3">
+										Processing at data center closest to End User
+									</td>
+									<td className="border border-border px-4 py-3">
+										Content delivery network and security
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Fivetran</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">ETL and data integration</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Fly.io</td>
+									<td className="border border-border px-4 py-3">Backend Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">
+										Application hosting and deployment
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">HubSpot</td>
+									<td className="border border-border px-4 py-3">Customer Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">CRM and marketing automation</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Loops</td>
+									<td className="border border-border px-4 py-3">Communication Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Email and customer communication</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Metabase</td>
+									<td className="border border-border px-4 py-3">Data Analytics</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">
+										Business intelligence and reporting
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">PostHog</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Product analytics</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Sentry</td>
+									<td className="border border-border px-4 py-3">All Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Error tracking and monitoring</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Snowflake</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Data warehousing and analytics</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Stripe</td>
+									<td className="border border-border px-4 py-3">Payment Services</td>
+									<td className="border border-border px-4 py-3">United States, Europe</td>
+									<td className="border border-border px-4 py-3">Payment processing and billing</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Supabase</td>
+									<td className="border border-border px-4 py-3">Data Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Database management and storage</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-3 font-medium">Upstash</td>
+									<td className="border border-border px-4 py-3">Infrastructure Services</td>
+									<td className="border border-border px-4 py-3">United States</td>
+									<td className="border border-border px-4 py-3">Serverless database services</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-3 font-medium">Vercel</td>
+									<td className="border border-border px-4 py-3">Customer-facing Services</td>
+									<td className="border border-border px-4 py-3">United States, Europe</td>
+									<td className="border border-border px-4 py-3">
+										Web application hosting and deployment
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</>
+	)
+}

+ 7 - 4
apps/web-roo-code/src/app/page.tsx

@@ -32,14 +32,17 @@ export default async function Home() {
 									<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
 										AI-Powered
 									</AnimatedText>
-									<span className="block">Dev Team, Right in Your Editor.</span>
+									<span className="block">Dev Team, in Your Editor</span>
+									<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
+										and Beyond
+									</AnimatedText>
 								</h1>
 								<p className="mt-4 max-w-md text-base text-muted-foreground sm:mt-6 sm:text-lg">
-									Supercharge your editor with AI that{" "}
+									Supercharge your software development with AI that{" "}
 									<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
 										understands your codebase
-									</AnimatedText>
-									, streamlines development, and helps you write, refactor, and debug with ease.
+									</AnimatedText>{" "}
+									and helps you write, refactor, and debug with ease in your editor and in the cloud.
 								</p>
 							</div>
 							<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">

+ 14 - 1
apps/web-roo-code/src/app/privacy/page.tsx

@@ -46,7 +46,7 @@ export default function Privacy() {
 					<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
 						Roo Code Cloud Privacy Policy
 					</h1>
-					<p className="text-muted-foreground">Last Updated: August 20, 2025</p>
+					<p className="text-muted-foreground">Last Updated: September 19, 2025</p>
 
 					<p className="lead">
 						This Privacy Policy explains how Roo Code, Inc. (&quot;Roo Code,&quot; &quot;we,&quot;
@@ -184,6 +184,13 @@ export default function Privacy() {
 						<li>
 							<strong>Send product updates and roadmap communications</strong> (opt‑out available)
 						</li>
+						<li>
+							<strong>Send onboarding, educational, and promotional communications</strong>. We may use
+							your account information (such as your name and email address) to send you onboarding
+							messages, product tutorials, feature announcements, newsletters, and other marketing
+							communications. You can opt out of non‑transactional emails at any time (see “Your Choices”
+							below).
+						</li>
 					</ul>
 
 					<h2 className="mt-12 text-2xl font-bold">3. Where Your Data Goes (And Doesn&apos;t)</h2>
@@ -277,6 +284,12 @@ export default function Privacy() {
 							<strong>Delete your Cloud account</strong> at any time from{" "}
 							<strong>Security Settings</strong> inside Roo Code Cloud.
 						</li>
+						<li>
+							<strong>Marketing communications:</strong> You can unsubscribe from marketing and
+							promotional emails by clicking the unsubscribe link in those emails. Transactional or
+							service‑related emails (such as password resets, billing notices, or security alerts) will
+							continue even if you opt out.
+						</li>
 					</ul>
 
 					<h2 className="mt-12 text-2xl font-bold">6. Security Practices</h2>

+ 111 - 0
apps/web-roo-code/src/components/CookieConsentWrapper.tsx

@@ -0,0 +1,111 @@
+"use client"
+
+import React, { useState, useEffect } from "react"
+import ReactCookieConsent from "react-cookie-consent"
+import { Cookie } from "lucide-react"
+import { getDomain } from "tldts"
+import { CONSENT_COOKIE_NAME } from "@roo-code/types"
+import { dispatchConsentEvent } from "@/lib/analytics/consent-manager"
+
+/**
+ * GDPR-compliant cookie consent banner component
+ * Handles both the UI and consent event dispatching
+ */
+export function CookieConsentWrapper() {
+	const [cookieDomain, setCookieDomain] = useState<string | null>(null)
+
+	useEffect(() => {
+		// Get the appropriate domain using tldts
+		if (typeof window !== "undefined") {
+			const domain = getDomain(window.location.hostname)
+			setCookieDomain(domain)
+		}
+	}, [])
+
+	const handleAccept = () => {
+		dispatchConsentEvent(true)
+	}
+
+	const handleDecline = () => {
+		dispatchConsentEvent(false)
+	}
+
+	const extraCookieOptions = cookieDomain
+		? {
+				domain: cookieDomain,
+			}
+		: {}
+
+	const containerClasses = `
+		fixed bottom-2 left-2 right-2 z-[999]
+		bg-black/95 dark:bg-white/95
+		text-white dark:text-black
+		border-t-neutral-800 dark:border-t-gray-200
+		backdrop-blur-xl
+		border-t
+		font-semibold
+		rounded-t-lg
+		px-4 py-4 md:px-8 md:py-4
+		flex flex-wrap items-center justify-between gap-4
+		text-sm font-sans
+	`.trim()
+
+	const buttonWrapperClasses = `
+		flex
+		flex-row-reverse
+		items-center
+		gap-2
+	`.trim()
+
+	const acceptButtonClasses = `
+		bg-white text-black border-neutral-800
+		dark:bg-black dark:text-white dark:border-gray-200
+		hover:opacity-50
+		transition-opacity
+		rounded-md
+		px-4 py-2 mr-2
+		text-sm font-bold
+		cursor-pointer
+		focus:outline-none focus:ring-2 focus:ring-offset-2
+	`.trim()
+
+	const declineButtonClasses = `
+		dark:bg-white dark:text-black dark:border-gray-200
+		bg-black text-white border-neutral-800
+		hover:opacity-50
+		border border-border
+		transition-opacity
+		rounded-md
+		px-4 py-2
+		text-sm font-bold
+		cursor-pointer
+		focus:outline-none focus:ring-2 focus:ring-offset-2
+	`.trim()
+
+	return (
+		<div role="banner" aria-label="Cookie consent banner" aria-live="polite">
+			<ReactCookieConsent
+				location="bottom"
+				buttonText="Accept"
+				declineButtonText="Decline"
+				cookieName={CONSENT_COOKIE_NAME}
+				expires={365}
+				enableDeclineButton={true}
+				onAccept={handleAccept}
+				onDecline={handleDecline}
+				containerClasses={containerClasses}
+				buttonClasses={acceptButtonClasses}
+				buttonWrapperClasses={buttonWrapperClasses}
+				declineButtonClasses={declineButtonClasses}
+				extraCookieOptions={extraCookieOptions}
+				disableStyles={true}
+				ariaAcceptLabel={`Accept`}
+				ariaDeclineLabel={`Decline`}>
+				<div className="flex items-center gap-2">
+					<Cookie className="size-5 hidden md:block" />
+					<span>Like most of the internet, we use cookies. Are you OK with that?</span>
+				</div>
+			</ReactCookieConsent>
+		</div>
+	)
+}

+ 14 - 0
apps/web-roo-code/src/components/chromes/footer.tsx

@@ -256,6 +256,20 @@ export function Footer() {
 											)}
 										</div>
 									</li>
+									<li>
+										<Link
+											href="/legal/cookies"
+											className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
+											Cookie Policy
+										</Link>
+									</li>
+									<li>
+										<Link
+											href="/legal/subprocessors"
+											className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
+											Subprocessors
+										</Link>
+									</li>
 								</ul>
 							</div>
 							<div className="mt-10 md:mt-0">

+ 92 - 0
apps/web-roo-code/src/components/providers/google-analytics-provider.tsx

@@ -0,0 +1,92 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import Script from "next/script"
+import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
+
+// Google Tag Manager ID
+const GTM_ID = "AW-17391954825"
+
+/**
+ * Google Analytics Provider
+ * Only loads Google Tag Manager after user gives consent
+ */
+export function GoogleAnalyticsProvider({ children }: { children: React.ReactNode }) {
+	const [shouldLoad, setShouldLoad] = useState(false)
+
+	useEffect(() => {
+		// Check initial consent status
+		if (hasConsent()) {
+			setShouldLoad(true)
+			initializeGoogleAnalytics()
+		}
+
+		// Listen for consent changes
+		const unsubscribe = onConsentChange((consented) => {
+			if (consented && !shouldLoad) {
+				setShouldLoad(true)
+				initializeGoogleAnalytics()
+			}
+		})
+
+		return unsubscribe
+	}, [shouldLoad])
+
+	const initializeGoogleAnalytics = () => {
+		// Initialize the dataLayer and gtag function
+		if (typeof window !== "undefined") {
+			window.dataLayer = window.dataLayer || []
+			window.gtag = function (...args: GtagArgs) {
+				window.dataLayer.push(args)
+			}
+			window.gtag("js", new Date())
+			window.gtag("config", GTM_ID)
+		}
+	}
+
+	// Only render Google Analytics scripts if consent is given
+	if (!shouldLoad) {
+		return <>{children}</>
+	}
+
+	return (
+		<>
+			{/* Google tag (gtag.js) - Only loads after consent */}
+			<Script
+				src={`https://www.googletagmanager.com/gtag/js?id=${GTM_ID}`}
+				strategy="afterInteractive"
+				onLoad={() => {
+					console.log("Google Analytics loaded with consent")
+				}}
+			/>
+			<Script id="google-analytics-init" strategy="afterInteractive">
+				{`
+					window.dataLayer = window.dataLayer || [];
+					function gtag(){dataLayer.push(arguments);}
+					gtag('js', new Date());
+					gtag('config', '${GTM_ID}');
+				`}
+			</Script>
+			{children}
+		</>
+	)
+}
+
+// Type definitions for Google Analytics
+type GtagArgs = ["js", Date] | ["config", string, GtagConfig?] | ["event", string, GtagEventParameters?]
+
+interface GtagConfig {
+	[key: string]: unknown
+}
+
+interface GtagEventParameters {
+	[key: string]: unknown
+}
+
+// Declare global types for TypeScript
+declare global {
+	interface Window {
+		dataLayer: GtagArgs[]
+		gtag: (...args: GtagArgs) => void
+	}
+}

+ 42 - 20
apps/web-roo-code/src/components/providers/posthog-provider.tsx

@@ -3,16 +3,15 @@
 import { usePathname, useSearchParams } from "next/navigation"
 import posthog from "posthog-js"
 import { PostHogProvider as OriginalPostHogProvider } from "posthog-js/react"
-import { useEffect, Suspense } from "react"
+import { useEffect, Suspense, useState } from "react"
+import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
 
-// Create a separate component for analytics tracking that uses useSearchParams
 function PageViewTracker() {
 	const pathname = usePathname()
 	const searchParams = useSearchParams()
 
 	// Track page views
 	useEffect(() => {
-		// Only track page views if PostHog is properly initialized
 		if (pathname && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
 			let url = window.location.origin + pathname
 			if (searchParams && searchParams.toString()) {
@@ -29,8 +28,10 @@ function PageViewTracker() {
 }
 
 export function PostHogProvider({ children }: { children: React.ReactNode }) {
+	const [isInitialized, setIsInitialized] = useState(false)
+
 	useEffect(() => {
-		// Initialize PostHog only on the client side
+		// Initialize PostHog only on the client side AND when consent is given
 		if (typeof window !== "undefined") {
 			const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
 			const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST
@@ -51,27 +52,48 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
 				)
 			}
 
-			posthog.init(posthogKey, {
-				api_host: posthogHost || "https://us.i.posthog.com",
-				capture_pageview: false, // We'll handle this manually
-				loaded: (posthogInstance) => {
-					if (process.env.NODE_ENV === "development") {
-						// Log to console in development
-						posthogInstance.debug()
-					}
-				},
-				respect_dnt: true, // Respect Do Not Track
+			const initializePosthog = () => {
+				if (!isInitialized) {
+					posthog.init(posthogKey, {
+						api_host: posthogHost || "https://us.i.posthog.com",
+						capture_pageview: false,
+						loaded: (posthogInstance) => {
+							if (process.env.NODE_ENV === "development") {
+								posthogInstance.debug()
+							}
+						},
+						respect_dnt: true, // Respect Do Not Track
+					})
+					setIsInitialized(true)
+				}
+			}
+
+			// Check initial consent status
+			if (hasConsent()) {
+				initializePosthog()
+			}
+
+			// Listen for consent changes
+			const unsubscribe = onConsentChange((consented) => {
+				if (consented && !isInitialized) {
+					initializePosthog()
+				}
 			})
-		}
 
-		// No explicit cleanup needed for posthog-js v1.231.0
-	}, [])
+			return () => {
+				unsubscribe()
+			}
+		}
+	}, [isInitialized])
 
+	// Only provide PostHog context if it's initialized
 	return (
 		<OriginalPostHogProvider client={posthog}>
-			<Suspense fallback={null}>
-				<PageViewTracker />
-			</Suspense>
+			{isInitialized && (
+				<Suspense fallback={null}>
+					<PageViewTracker />
+				</Suspense>
+			)}
 			{children}
 		</OriginalPostHogProvider>
 	)

+ 8 - 5
apps/web-roo-code/src/components/providers/providers.tsx

@@ -4,17 +4,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 import { ThemeProvider } from "next-themes"
 
 import { PostHogProvider } from "./posthog-provider"
+import { GoogleAnalyticsProvider } from "./google-analytics-provider"
 
 const queryClient = new QueryClient()
 
 export const Providers = ({ children }: { children: React.ReactNode }) => {
 	return (
 		<QueryClientProvider client={queryClient}>
-			<PostHogProvider>
-				<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
-					{children}
-				</ThemeProvider>
-			</PostHogProvider>
+			<GoogleAnalyticsProvider>
+				<PostHogProvider>
+					<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
+						{children}
+					</ThemeProvider>
+				</PostHogProvider>
+			</GoogleAnalyticsProvider>
 		</QueryClientProvider>
 	)
 }

+ 47 - 0
apps/web-roo-code/src/lib/analytics/consent-manager.ts

@@ -0,0 +1,47 @@
+/**
+ * Simple consent event system
+ * Dispatches events when cookie consent changes
+ */
+
+import { getCookieConsentValue } from "react-cookie-consent"
+import { CONSENT_COOKIE_NAME } from "@roo-code/types"
+
+export const CONSENT_EVENT = "cookieConsentChanged"
+
+/**
+ * Check if user has given consent for analytics cookies
+ * Uses react-cookie-consent's built-in function
+ */
+export function hasConsent(): boolean {
+	if (typeof window === "undefined") return false
+	return getCookieConsentValue(CONSENT_COOKIE_NAME) === "true"
+}
+
+/**
+ * Dispatch a consent change event
+ */
+export function dispatchConsentEvent(consented: boolean): void {
+	if (typeof window !== "undefined") {
+		const event = new CustomEvent(CONSENT_EVENT, {
+			detail: { consented },
+		})
+		window.dispatchEvent(event)
+	}
+}
+
+/**
+ * Listen for consent changes
+ */
+export function onConsentChange(callback: (consented: boolean) => void): () => void {
+	if (typeof window === "undefined") {
+		return () => {}
+	}
+
+	const handler = (event: Event) => {
+		const customEvent = event as CustomEvent<{ consented: boolean }>
+		callback(customEvent.detail.consented)
+	}
+
+	window.addEventListener(CONSENT_EVENT, handler)
+	return () => window.removeEventListener(CONSENT_EVENT, handler)
+}

+ 1 - 1
packages/build/src/__tests__/index.test.ts

@@ -148,7 +148,7 @@ describe("generatePackageJson", () => {
 					{
 						command: "roo-code-nightly.plusButtonClicked",
 						title: "%command.newTask.title%",
-						icon: "$(add)",
+						icon: "$(edit)",
 					},
 					{
 						command: "roo-code-nightly.openInNewTab",

+ 12 - 3
packages/build/src/esbuild.ts

@@ -2,7 +2,7 @@ import * as fs from "fs"
 import * as path from "path"
 import { execSync } from "child_process"
 
-import { ViewsContainer, Views, Menus, Configuration, contributesSchema } from "./types.js"
+import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js"
 
 function copyDir(srcDir: string, dstDir: string, count: number): number {
 	const entries = fs.readdirSync(srcDir, { withFileTypes: true })
@@ -216,10 +216,12 @@ export function generatePackageJson({
 	overrideJson: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
 	substitution: [string, string]
 }) {
-	const { viewsContainers, views, commands, menus, submenus, configuration } = contributesSchema.parse(contributes)
+	const { viewsContainers, views, commands, menus, submenus, keybindings, configuration } =
+		contributesSchema.parse(contributes)
 	const [from, to] = substitution
 
-	return {
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	const result: Record<string, any> = {
 		...packageJson,
 		...overrideJson,
 		contributes: {
@@ -234,6 +236,13 @@ export function generatePackageJson({
 			},
 		},
 	}
+
+	// Only add keybindings if they exist
+	if (keybindings) {
+		result.contributes.keybindings = transformArray<Keybindings>(keybindings, from, to, "command")
+	}
+
+	return result
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any

+ 14 - 0
packages/build/src/types.ts

@@ -59,6 +59,19 @@ const submenusSchema = z.array(
 
 export type Submenus = z.infer<typeof submenusSchema>
 
+const keybindingsSchema = z.array(
+	z.object({
+		command: z.string(),
+		key: z.string().optional(),
+		mac: z.string().optional(),
+		win: z.string().optional(),
+		linux: z.string().optional(),
+		when: z.string().optional(),
+	}),
+)
+
+export type Keybindings = z.infer<typeof keybindingsSchema>
+
 const configurationPropertySchema = z.object({
 	type: z.union([
 		z.literal("string"),
@@ -92,6 +105,7 @@ export const contributesSchema = z.object({
 	commands: commandsSchema,
 	menus: menusSchema,
 	submenus: submenusSchema,
+	keybindings: keybindingsSchema.optional(),
 	configuration: configurationSchema,
 })
 

+ 112 - 3
packages/cloud/src/CloudService.ts

@@ -8,6 +8,7 @@ import type {
 	AuthService,
 	SettingsService,
 	CloudUserInfo,
+	CloudOrganizationMembership,
 	OrganizationAllowList,
 	OrganizationSettings,
 	ShareVisibility,
@@ -24,6 +25,7 @@ import { StaticSettingsService } from "./StaticSettingsService.js"
 import { CloudTelemetryClient as TelemetryClient } from "./TelemetryClient.js"
 import { CloudShareService } from "./CloudShareService.js"
 import { CloudAPI } from "./CloudAPI.js"
+import { RetryQueue } from "./retry-queue/index.js"
 
 type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0]
 type AuthUserInfoPayload = CloudServiceEvents["user-info"][0]
@@ -75,6 +77,12 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 		return this._cloudAPI
 	}
 
+	private _retryQueue: RetryQueue | null = null
+
+	public get retryQueue() {
+		return this._retryQueue
+	}
+
 	private constructor(context: ExtensionContext, log?: (...args: unknown[]) => void) {
 		super()
 
@@ -82,6 +90,8 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 		this.log = log || console.log
 
 		this.authStateListener = (data: AuthStateChangedPayload) => {
+			// Handle retry queue based on auth state changes
+			this.handleAuthStateChangeForRetryQueue(data)
 			this.emit("auth-state-changed", data)
 		}
 
@@ -131,7 +141,24 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 
 			this._cloudAPI = new CloudAPI(this._authService, this.log)
 
-			this._telemetryClient = new TelemetryClient(this._authService, this._settingsService)
+			// Initialize retry queue with auth header provider
+			this._retryQueue = new RetryQueue(
+				this.context,
+				undefined, // Use default config
+				this.log,
+				() => {
+					// Provide fresh auth headers for retries
+					const sessionToken = this._authService?.getSessionToken()
+					if (sessionToken) {
+						return {
+							Authorization: `Bearer ${sessionToken}`,
+						}
+					}
+					return undefined
+				},
+			)
+
+			this._telemetryClient = new TelemetryClient(this._authService, this._settingsService, this._retryQueue)
 
 			this._shareService = new CloudShareService(this._cloudAPI, this._settingsService, this.log)
 
@@ -144,9 +171,9 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 
 	// AuthService
 
-	public async login(): Promise<void> {
+	public async login(landingPageSlug?: string): Promise<void> {
 		this.ensureInitialized()
-		return this.authService!.login()
+		return this.authService!.login(landingPageSlug)
 	}
 
 	public async logout(): Promise<void> {
@@ -216,6 +243,21 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 		return this.authService!.handleCallback(code, state, organizationId)
 	}
 
+	public async switchOrganization(organizationId: string | null): Promise<void> {
+		this.ensureInitialized()
+
+		// Perform the organization switch
+		// StaticTokenAuthService will throw an error if organization switching is not supported
+		await this.authService!.switchOrganization(organizationId)
+	}
+
+	public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
+		this.ensureInitialized()
+
+		// StaticTokenAuthService will throw an error if organization memberships are not supported
+		return await this.authService!.getOrganizationMemberships()
+	}
+
 	// SettingsService
 
 	public getAllowList(): OrganizationAllowList {
@@ -303,6 +345,10 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 			this.settingsService.dispose()
 		}
 
+		if (this._retryQueue) {
+			this._retryQueue.dispose()
+		}
+
 		this.isInitialized = false
 	}
 
@@ -365,4 +411,67 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
 	static isEnabled(): boolean {
 		return !!this._instance?.isAuthenticated()
 	}
+
+	/**
+	 * Handle auth state changes for the retry queue
+	 * - Pause queue when not in 'active-session' state
+	 * - Clear queue when user logs out or logs in as different user
+	 * - Resume queue when returning to active-session with same user
+	 */
+	private handleAuthStateChangeForRetryQueue(data: AuthStateChangedPayload): void {
+		if (!this._retryQueue) {
+			return
+		}
+
+		const newState = data.state
+		const userInfo = this.getUserInfo()
+		const newUserId = userInfo?.id
+
+		this.log(`[CloudService] Auth state changed to: ${newState}, user: ${newUserId}`)
+
+		// Handle different auth states
+		switch (newState) {
+			case "active-session": {
+				// Check if user changed (different user logged in)
+				const wasCleared = this._retryQueue.clearIfUserChanged(newUserId)
+
+				if (!wasCleared) {
+					// Same user or first login, resume the queue
+					this._retryQueue.resume()
+					this.log("[CloudService] Resuming retry queue for active session")
+				} else {
+					// Different user, queue was cleared, but we can resume processing
+					this._retryQueue.resume()
+					this.log("[CloudService] Retry queue cleared for new user, resuming processing")
+				}
+				break
+			}
+
+			case "logged-out":
+				// User is logged out, clear the queue
+				this._retryQueue.clearIfUserChanged(undefined)
+				this._retryQueue.pause()
+				this.log("[CloudService] Pausing and clearing retry queue for logged-out state")
+				break
+
+			case "initializing":
+			case "attempting-session":
+				// Transitional states, pause the queue but don't clear
+				this._retryQueue.pause()
+				this.log(`[CloudService] Pausing retry queue during ${newState}`)
+				break
+
+			case "inactive-session":
+				// Session is inactive (possibly expired), pause but don't clear
+				// The queue might resume if the session becomes active again
+				this._retryQueue.pause()
+				this.log("[CloudService] Pausing retry queue for inactive session")
+				break
+
+			default:
+				// Unknown state, pause as a safety measure
+				this._retryQueue.pause()
+				this.log(`[CloudService] Pausing retry queue for unknown state: ${newState}`)
+		}
+	}
 }

+ 8 - 0
packages/cloud/src/StaticTokenAuthService.ts

@@ -63,6 +63,14 @@ export class StaticTokenAuthService extends EventEmitter<AuthServiceEvents> impl
 		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
 	}
 
+	public async switchOrganization(_organizationId: string | null): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public async getOrganizationMemberships(): Promise<import("@roo-code/types").CloudOrganizationMembership[]> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
 	public getState(): AuthState {
 		return this.state
 	}

+ 57 - 24
packages/cloud/src/TelemetryClient.ts

@@ -12,6 +12,7 @@ import {
 } from "@roo-code/types"
 
 import { getRooCodeApiUrl } from "./config.js"
+import type { RetryQueue } from "./retry-queue/index.js"
 
 abstract class BaseTelemetryClient implements TelemetryClient {
 	protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
@@ -87,22 +88,22 @@ abstract class BaseTelemetryClient implements TelemetryClient {
 }
 
 export class CloudTelemetryClient extends BaseTelemetryClient {
+	private retryQueue: RetryQueue | null = null
+
 	constructor(
 		private authService: AuthService,
 		private settingsService: SettingsService,
-		debug = false,
+		retryQueue?: RetryQueue,
 	) {
-		super(
-			{
-				type: "exclude",
-				events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
-			},
-			debug,
-		)
+		super({
+			type: "exclude",
+			events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
+		})
+		this.retryQueue = retryQueue || null
 	}
 
 	// kilocode_change
-	private async fetch(path: string, options: RequestInit) {
+	private async fetch(path: string, options: RequestInit, allowQueueing = true) {
 		if (!this.authService.isAuthenticated()) {
 			return
 		}
@@ -121,12 +122,39 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
 				Authorization: `Bearer ${token}`,
 				"Content-Type": "application/json",
 			},
-		})
+		}
 
-		if (!response.ok) {
-			console.error(
-				`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
-			)
+		try {
+			const response = await fetch(url, fetchOptions)
+
+			if (!response.ok) {
+				console.error(
+					`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
+				)
+
+				// Queue for retry on server errors (5xx) or rate limiting (429)
+				// Do NOT retry on client errors (4xx) except 429 - they won't succeed
+				if (this.retryQueue && allowQueueing && (response.status >= 500 || response.status === 429)) {
+					await this.retryQueue.enqueue(url, fetchOptions, "telemetry")
+				}
+			}
+
+			return response
+		} catch (error) {
+			console.error(`[TelemetryClient#fetch] Network error for ${options.method} ${path}: ${error}`)
+
+			// Queue for retry on network failures (typically TypeError with "fetch failed" message)
+			// These are transient network issues that may succeed on retry
+			if (
+				this.retryQueue &&
+				allowQueueing &&
+				error instanceof TypeError &&
+				error.message.includes("fetch failed")
+			) {
+				await this.retryQueue.enqueue(url, fetchOptions, "telemetry")
+			}
+
+			throw error
 		}
 			*/
 	}
@@ -168,6 +196,7 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
 			})
 		} catch (error) {
 			console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
+			// Error is already queued for retry in the fetch method
 		}
 		*/
 	}
@@ -211,22 +240,26 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
 				)
 			}
 
-			// Custom fetch for multipart - don't set Content-Type header (let browser set it)
-			const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, {
+			const url = `${getRooCodeApiUrl()}/api/events/backfill`
+			const fetchOptions: RequestInit = {
 				method: "POST",
 				headers: {
 					Authorization: `Bearer ${token}`,
-					// Note: No Content-Type header - browser will set multipart/form-data with boundary
 				},
 				body: formData,
-			})
+			}
 
-			if (!response.ok) {
-				console.error(
-					`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
-				)
-			} else if (this.debug) {
-				console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`)
+			try {
+				const response = await fetch(url, fetchOptions)
+
+				if (!response.ok) {
+					console.error(
+						`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
+					)
+				}
+			} catch (fetchError) {
+				console.error(`[TelemetryClient#backfillMessages] Network error: ${fetchError}`)
+				throw fetchError
 			}
 		} catch (error) {
 			console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)

+ 57 - 6
packages/cloud/src/WebAuthService.ts

@@ -141,7 +141,8 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 				if (
 					this.credentials === null ||
 					this.credentials.clientToken !== credentials.clientToken ||
-					this.credentials.sessionId !== credentials.sessionId
+					this.credentials.sessionId !== credentials.sessionId ||
+					this.credentials.organizationId !== credentials.organizationId
 				) {
 					this.transitionToAttemptingSession(credentials)
 				}
@@ -174,6 +175,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 
 		this.changeState("attempting-session")
 
+		this.timer.stop()
 		this.timer.start()
 	}
 
@@ -248,8 +250,10 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 	 *
 	 * This method initiates the authentication flow by generating a state parameter
 	 * and opening the browser to the authorization URL.
+	 *
+	 * @param landingPageSlug Optional slug of a specific landing page (e.g., "supernova", "special-offer", etc.)
 	 */
-	public async login(): Promise<void> {
+	public async login(landingPageSlug?: string): Promise<void> {
 		try {
 			const vscode = await importVscode()
 
@@ -267,11 +271,17 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 				state,
 				auth_redirect: `${vscode.env.uriScheme}://${publisher}.${name}`,
 			})
-			const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}`
+
+			// Use landing page URL if slug is provided, otherwise use default sign-in URL
+			const url = landingPageSlug
+				? `${getRooCodeApiUrl()}/l/${landingPageSlug}?${params.toString()}`
+				: `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}`
+
 			await vscode.env.openExternal(vscode.Uri.parse(url))
 		} catch (error) {
-			this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
-			throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`)
+			const context = landingPageSlug ? ` (landing page: ${landingPageSlug})` : ""
+			this.log(`[auth] Error initiating Roo Code Cloud auth${context}: ${error}`)
+			throw new Error(`Failed to initiate Roo Code Cloud authentication${context}: ${error}`)
 		}
 	}
 
@@ -461,6 +471,42 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 		return this.credentials?.organizationId || null
 	}
 
+	/**
+	 * Switch to a different organization context
+	 * @param organizationId The organization ID to switch to, or null for personal account
+	 */
+	public async switchOrganization(organizationId: string | null): Promise<void> {
+		if (!this.credentials) {
+			throw new Error("Cannot switch organization: not authenticated")
+		}
+
+		// Update the stored credentials with the new organization ID
+		const updatedCredentials: AuthCredentials = {
+			...this.credentials,
+			organizationId: organizationId,
+		}
+
+		// Store the updated credentials, handleCredentialsChange will handle the update
+		await this.storeCredentials(updatedCredentials)
+	}
+
+	/**
+	 * Get all organization memberships for the current user
+	 * @returns Array of organization memberships
+	 */
+	public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
+		if (!this.credentials) {
+			return []
+		}
+
+		try {
+			return await this.clerkGetOrganizationMemberships()
+		} catch (error) {
+			this.log(`[auth] Failed to get organization memberships: ${error}`)
+			return []
+		}
+	}
+
 	private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
 		const formData = new URLSearchParams()
 		formData.append("strategy", "ticket")
@@ -645,9 +691,14 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 	}
 
 	private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
+		if (!this.credentials) {
+			this.log("[auth] Cannot get organization memberships: missing credentials")
+			return []
+		}
+
 		const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
 			headers: {
-				Authorization: `Bearer ${this.credentials!.clientToken}`,
+				Authorization: `Bearer ${this.credentials.clientToken}`,
 				"User-Agent": this.userAgent(),
 			},
 			signal: AbortSignal.timeout(10000),

+ 0 - 22
packages/cloud/src/__tests__/TelemetryClient.test.ts

@@ -642,28 +642,6 @@ describe.skip("TelemetryClient", () => {
 			)
 		})
 
-		it("should log debug information when debug is enabled", async () => {
-			const client = new TelemetryClient(mockAuthService, mockSettingsService, true)
-
-			const messages = [
-				{
-					ts: 1,
-					type: "say" as const,
-					say: "text" as const,
-					text: "test message",
-				},
-			]
-
-			await client.backfillMessages(messages, "test-task-id")
-
-			expect(console.info).toHaveBeenCalledWith(
-				"[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
-			)
-			expect(console.info).toHaveBeenCalledWith(
-				"[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
-			)
-		})
-
 		it("should handle empty messages array", async () => {
 			const client = new TelemetryClient(mockAuthService, mockSettingsService)
 

+ 3 - 0
packages/cloud/src/index.ts

@@ -3,3 +3,6 @@ export * from "./config.js"
 export { CloudService } from "./CloudService.js"
 
 export { BridgeOrchestrator } from "./bridge/index.js"
+
+export { RetryQueue } from "./retry-queue/index.js"
+export type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./retry-queue/index.js"

+ 371 - 0
packages/cloud/src/retry-queue/RetryQueue.ts

@@ -0,0 +1,371 @@
+import { EventEmitter } from "events"
+import type { ExtensionContext } from "vscode"
+import type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./types.js"
+
+type AuthHeaderProvider = () => Record<string, string> | undefined
+
+export class RetryQueue extends EventEmitter<RetryQueueEvents> {
+	private queue: Map<string, QueuedRequest> = new Map()
+	private context: ExtensionContext
+	private config: RetryQueueConfig
+	private log: (...args: unknown[]) => void
+	private isProcessing = false
+	private retryTimer?: NodeJS.Timeout
+	private readonly STORAGE_KEY = "roo.retryQueue"
+	private authHeaderProvider?: AuthHeaderProvider
+	private queuePausedUntil?: number // Timestamp when the queue can resume processing
+	private isPaused = false // Manual pause state (e.g., for auth state changes)
+	private currentUserId?: string // Track current user ID for conditional clearing
+	private hasHadUser = false // Track if we've ever had a user (to distinguish from first login)
+
+	constructor(
+		context: ExtensionContext,
+		config?: Partial<RetryQueueConfig>,
+		log?: (...args: unknown[]) => void,
+		authHeaderProvider?: AuthHeaderProvider,
+	) {
+		super()
+		this.context = context
+		this.log = log || console.log
+		this.authHeaderProvider = authHeaderProvider
+
+		this.config = {
+			maxRetries: 0,
+			retryDelay: 60000,
+			maxQueueSize: 100,
+			persistQueue: true,
+			networkCheckInterval: 60000,
+			requestTimeout: 30000,
+			...config,
+		}
+
+		this.loadPersistedQueue()
+		this.startRetryTimer()
+	}
+
+	private loadPersistedQueue(): void {
+		if (!this.config.persistQueue) return
+
+		try {
+			const stored = this.context.workspaceState.get<QueuedRequest[]>(this.STORAGE_KEY)
+			if (stored && Array.isArray(stored)) {
+				stored.forEach((request) => {
+					this.queue.set(request.id, request)
+				})
+				this.log(`[RetryQueue] Loaded ${stored.length} persisted requests from workspace storage`)
+			}
+		} catch (error) {
+			this.log("[RetryQueue] Failed to load persisted queue:", error)
+		}
+	}
+
+	private async persistQueue(): Promise<void> {
+		if (!this.config.persistQueue) return
+
+		try {
+			const requests = Array.from(this.queue.values())
+			await this.context.workspaceState.update(this.STORAGE_KEY, requests)
+		} catch (error) {
+			this.log("[RetryQueue] Failed to persist queue:", error)
+		}
+	}
+
+	public async enqueue(
+		url: string,
+		options: RequestInit,
+		type: QueuedRequest["type"] = "other",
+		operation?: string,
+	): Promise<void> {
+		if (this.queue.size >= this.config.maxQueueSize) {
+			const oldestId = Array.from(this.queue.keys())[0]
+			if (oldestId) {
+				this.queue.delete(oldestId)
+			}
+		}
+
+		const request: QueuedRequest = {
+			id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
+			url,
+			options,
+			timestamp: Date.now(),
+			retryCount: 0,
+			type,
+			operation,
+		}
+
+		this.queue.set(request.id, request)
+		await this.persistQueue()
+
+		this.emit("request-queued", request)
+		this.log(`[RetryQueue] Queued request: ${url}`)
+	}
+
+	public async retryAll(): Promise<void> {
+		if (this.isProcessing) {
+			this.log("[RetryQueue] Already processing, skipping retry cycle")
+			return
+		}
+
+		// Check if the queue is manually paused (e.g., due to auth state)
+		if (this.isPaused) {
+			this.log("[RetryQueue] Queue is manually paused")
+			return
+		}
+
+		// Check if the entire queue is paused due to rate limiting
+		if (this.queuePausedUntil && Date.now() < this.queuePausedUntil) {
+			this.log(`[RetryQueue] Queue is paused until ${new Date(this.queuePausedUntil).toISOString()}`)
+			return
+		}
+
+		const requests = Array.from(this.queue.values())
+		if (requests.length === 0) {
+			return
+		}
+
+		this.isProcessing = true
+
+		try {
+			// Sort by timestamp to process in FIFO order (oldest first)
+			requests.sort((a, b) => a.timestamp - b.timestamp)
+
+			// Process all requests in FIFO order
+			for (const request of requests) {
+				try {
+					const response = await this.retryRequest(request)
+
+					// Check if we got a 429 rate limiting response
+					if (response && response.status === 429) {
+						const retryAfter = response.headers.get("Retry-After")
+						if (retryAfter) {
+							// Parse Retry-After (could be seconds or a date)
+							let delayMs: number
+							const retryAfterSeconds = parseInt(retryAfter, 10)
+							if (!isNaN(retryAfterSeconds)) {
+								delayMs = retryAfterSeconds * 1000
+							} else {
+								// Try parsing as a date
+								const retryDate = new Date(retryAfter)
+								if (!isNaN(retryDate.getTime())) {
+									delayMs = retryDate.getTime() - Date.now()
+								} else {
+									delayMs = 60000 // Default to 1 minute if we can't parse
+								}
+							}
+							// Pause the entire queue
+							this.queuePausedUntil = Date.now() + delayMs
+							this.log(`[RetryQueue] Rate limited, pausing entire queue for ${delayMs}ms`)
+							// Keep the request in the queue for later retry
+							this.queue.set(request.id, request)
+							// Stop processing further requests since the queue is paused
+							break
+						}
+					}
+
+					this.queue.delete(request.id)
+					this.emit("request-retry-success", request)
+				} catch (error) {
+					request.retryCount++
+					request.lastError = error instanceof Error ? error.message : String(error)
+
+					// Check if we've exceeded max retries
+					if (this.config.maxRetries > 0 && request.retryCount >= this.config.maxRetries) {
+						this.log(
+							`[RetryQueue] Max retries (${this.config.maxRetries}) reached for request: ${request.url}`,
+						)
+						this.queue.delete(request.id)
+						this.emit("request-max-retries-exceeded", request, error as Error)
+					} else {
+						this.queue.set(request.id, request)
+						this.emit("request-retry-failed", request, error as Error)
+					}
+
+					// Add a small delay between retry attempts
+					await this.delay(100)
+				}
+			}
+
+			await this.persistQueue()
+		} finally {
+			// Always reset the processing flag, even if an error occurs
+			this.isProcessing = false
+		}
+	}
+
+	private async retryRequest(request: QueuedRequest): Promise<Response> {
+		this.log(`[RetryQueue] Retrying request: ${request.url}`)
+
+		let headers = { ...request.options.headers }
+		if (this.authHeaderProvider) {
+			const freshAuthHeaders = this.authHeaderProvider()
+			if (freshAuthHeaders) {
+				headers = {
+					...headers,
+					...freshAuthHeaders,
+				}
+			}
+		}
+
+		const controller = new AbortController()
+		const timeoutId = setTimeout(() => controller.abort(), this.config.requestTimeout)
+
+		try {
+			const response = await fetch(request.url, {
+				...request.options,
+				signal: controller.signal,
+				headers: {
+					...headers,
+					"X-Retry-Queue": "true",
+				},
+			})
+
+			clearTimeout(timeoutId)
+
+			// Check for error status codes that should trigger retry
+			if (!response.ok) {
+				// Handle different status codes appropriately
+				if (response.status >= 500) {
+					// Server errors (5xx) should be retried
+					throw new Error(`Server error: ${response.status} ${response.statusText}`)
+				} else if (response.status === 429) {
+					// Rate limiting - return response to let caller handle Retry-After
+					return response
+				} else if (response.status >= 400 && response.status < 500) {
+					// Client errors (4xx including 401/403) should NOT be retried
+					// These errors indicate problems with the request itself that won't be fixed by retrying
+					this.log(`[RetryQueue] Non-retryable client error ${response.status}, removing from queue`)
+					return response
+				}
+			}
+
+			return response
+		} catch (error) {
+			clearTimeout(timeoutId)
+			throw error
+		}
+	}
+
+	private startRetryTimer(): void {
+		if (this.retryTimer) {
+			clearInterval(this.retryTimer)
+		}
+
+		this.retryTimer = setInterval(() => {
+			this.retryAll().catch((error) => {
+				this.log("[RetryQueue] Error during retry cycle:", error)
+			})
+		}, this.config.networkCheckInterval)
+	}
+
+	private delay(ms: number): Promise<void> {
+		return new Promise((resolve) => setTimeout(resolve, ms))
+	}
+
+	public getStats(): QueueStats {
+		const requests = Array.from(this.queue.values())
+		const byType: Record<string, number> = {}
+		let totalRetries = 0
+		let failedRetries = 0
+
+		requests.forEach((request) => {
+			byType[request.type] = (byType[request.type] || 0) + 1
+			totalRetries += request.retryCount
+			if (request.lastError) {
+				failedRetries++
+			}
+		})
+
+		const timestamps = requests.map((r) => r.timestamp)
+		const oldestRequest = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : undefined
+		const newestRequest = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : undefined
+
+		return {
+			totalQueued: requests.length,
+			byType,
+			oldestRequest,
+			newestRequest,
+			totalRetries,
+			failedRetries,
+		}
+	}
+
+	public clear(): void {
+		this.queue.clear()
+		this.persistQueue().catch((error) => {
+			this.log("[RetryQueue] Failed to persist after clear:", error)
+		})
+		this.emit("queue-cleared")
+	}
+
+	/**
+	 * Pause the retry queue. When paused, no retries will be processed.
+	 * This is useful when auth state is not active or during logout.
+	 */
+	public pause(): void {
+		this.isPaused = true
+		this.log("[RetryQueue] Queue paused")
+	}
+
+	/**
+	 * Resume the retry queue. Retries will be processed again on the next interval.
+	 */
+	public resume(): void {
+		this.isPaused = false
+		this.log("[RetryQueue] Queue resumed")
+	}
+
+	/**
+	 * Check if the queue is paused
+	 */
+	public isPausedState(): boolean {
+		return this.isPaused
+	}
+
+	/**
+	 * Set the current user ID for tracking user changes
+	 */
+	public setCurrentUserId(userId: string | undefined): void {
+		this.currentUserId = userId
+	}
+
+	/**
+	 * Get the current user ID
+	 */
+	public getCurrentUserId(): string | undefined {
+		return this.currentUserId
+	}
+
+	/**
+	 * Conditionally clear the queue based on user ID change.
+	 * If newUserId is different from currentUserId, clear the queue.
+	 * Returns true if queue was cleared, false otherwise.
+	 */
+	public clearIfUserChanged(newUserId: string | undefined): boolean {
+		// First time ever setting a user (initial login)
+		if (!this.hasHadUser && newUserId !== undefined) {
+			this.currentUserId = newUserId
+			this.hasHadUser = true
+			return false
+		}
+
+		// If user IDs are different (including logout case where newUserId is undefined)
+		if (this.currentUserId !== newUserId) {
+			this.log(`[RetryQueue] User changed from ${this.currentUserId} to ${newUserId}, clearing queue`)
+			this.clear()
+			this.currentUserId = newUserId
+			if (newUserId !== undefined) {
+				this.hasHadUser = true
+			}
+			return true
+		}
+
+		return false
+	}
+
+	public dispose(): void {
+		if (this.retryTimer) {
+			clearInterval(this.retryTimer)
+		}
+		this.removeAllListeners()
+	}
+}

+ 698 - 0
packages/cloud/src/retry-queue/__tests__/RetryQueue.test.ts

@@ -0,0 +1,698 @@
+import type { ExtensionContext } from "vscode"
+import { RetryQueue } from "../RetryQueue.js"
+import type { QueuedRequest } from "../types.js"
+
+// Mock ExtensionContext
+const createMockContext = (): ExtensionContext => {
+	const storage = new Map<string, unknown>()
+
+	return {
+		workspaceState: {
+			get: vi.fn((key: string) => storage.get(key)),
+			update: vi.fn(async (key: string, value: unknown) => {
+				storage.set(key, value)
+			}),
+		},
+	} as unknown as ExtensionContext
+}
+
+describe("RetryQueue", () => {
+	let mockContext: ExtensionContext
+	let retryQueue: RetryQueue
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockContext = createMockContext()
+		retryQueue = new RetryQueue(mockContext)
+	})
+
+	afterEach(() => {
+		retryQueue.dispose()
+	})
+
+	describe("enqueue", () => {
+		it("should add a request to the queue", async () => {
+			const url = "https://api.example.com/test"
+			const options = { method: "POST", body: JSON.stringify({ test: "data" }) }
+
+			await retryQueue.enqueue(url, options, "telemetry")
+
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1)
+			expect(stats.byType["telemetry"]).toBe(1)
+		})
+
+		it("should enforce max queue size with FIFO eviction", async () => {
+			// Create a queue with max size of 3
+			retryQueue = new RetryQueue(mockContext, { maxQueueSize: 3 })
+
+			// Add 4 requests
+			for (let i = 1; i <= 4; i++) {
+				await retryQueue.enqueue(`https://api.example.com/test${i}`, { method: "POST" }, "telemetry")
+			}
+
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(3) // Should only have 3 items (oldest was evicted)
+		})
+	})
+
+	describe("persistence", () => {
+		it("should persist queue to workspace state", async () => {
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			expect(mockContext.workspaceState.update).toHaveBeenCalledWith(
+				"roo.retryQueue",
+				expect.arrayContaining([
+					expect.objectContaining({
+						url: "https://api.example.com/test",
+						type: "telemetry",
+					}),
+				]),
+			)
+		})
+
+		it("should load persisted queue on initialization", () => {
+			const persistedRequests: QueuedRequest[] = [
+				{
+					id: "test-1",
+					url: "https://api.example.com/test1",
+					options: { method: "POST" },
+					timestamp: Date.now(),
+					retryCount: 0,
+					type: "telemetry",
+				},
+			]
+
+			// Set up mock to return persisted data
+			const storage = new Map([["roo.retryQueue", persistedRequests]])
+			mockContext = {
+				workspaceState: {
+					get: vi.fn((key: string) => storage.get(key)),
+					update: vi.fn(),
+				},
+			} as unknown as ExtensionContext
+
+			retryQueue = new RetryQueue(mockContext)
+
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1)
+			expect(mockContext.workspaceState.get).toHaveBeenCalledWith("roo.retryQueue")
+		})
+	})
+
+	describe("clear", () => {
+		it("should clear all queued requests", async () => {
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
+
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+
+			retryQueue.clear()
+
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+	})
+
+	describe("getStats", () => {
+		it("should return correct statistics", async () => {
+			const now = Date.now()
+
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
+			await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
+
+			const stats = retryQueue.getStats()
+
+			expect(stats.totalQueued).toBe(3)
+			expect(stats.byType["telemetry"]).toBe(2)
+			expect(stats.byType["api-call"]).toBe(1)
+			expect(stats.oldestRequest).toBeDefined()
+			expect(stats.newestRequest).toBeDefined()
+			expect(stats.oldestRequest!.getTime()).toBeGreaterThanOrEqual(now)
+			expect(stats.newestRequest!.getTime()).toBeGreaterThanOrEqual(now)
+		})
+	})
+
+	describe("events", () => {
+		it("should emit request-queued event when enqueueing", async () => {
+			const listener = vi.fn()
+			retryQueue.on("request-queued", listener)
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			expect(listener).toHaveBeenCalledWith(
+				expect.objectContaining({
+					url: "https://api.example.com/test",
+					type: "telemetry",
+				}),
+			)
+		})
+
+		it("should emit queue-cleared event when clearing", () => {
+			const listener = vi.fn()
+			retryQueue.on("queue-cleared", listener)
+
+			retryQueue.clear()
+
+			expect(listener).toHaveBeenCalled()
+		})
+	})
+
+	describe("auth state management", () => {
+		it("should pause and resume the queue", () => {
+			expect(retryQueue.isPausedState()).toBe(false)
+
+			retryQueue.pause()
+			expect(retryQueue.isPausedState()).toBe(true)
+
+			retryQueue.resume()
+			expect(retryQueue.isPausedState()).toBe(false)
+		})
+
+		it("should not process retries when paused", async () => {
+			const fetchMock = vi.fn().mockResolvedValue({ ok: true })
+			global.fetch = fetchMock
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Pause the queue
+			retryQueue.pause()
+
+			// Try to retry all
+			await retryQueue.retryAll()
+
+			// Fetch should not be called because queue is paused
+			expect(fetchMock).not.toHaveBeenCalled()
+
+			// Resume and retry
+			retryQueue.resume()
+			await retryQueue.retryAll()
+
+			// Now fetch should be called
+			expect(fetchMock).toHaveBeenCalledTimes(1)
+		})
+
+		it("should track and update current user ID", () => {
+			expect(retryQueue.getCurrentUserId()).toBeUndefined()
+
+			retryQueue.setCurrentUserId("user_123")
+			expect(retryQueue.getCurrentUserId()).toBe("user_123")
+
+			retryQueue.setCurrentUserId("user_456")
+			expect(retryQueue.getCurrentUserId()).toBe("user_456")
+
+			retryQueue.setCurrentUserId(undefined)
+			expect(retryQueue.getCurrentUserId()).toBeUndefined()
+		})
+
+		it("should clear queue when user changes", async () => {
+			// Add some requests
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+
+			// Set initial user
+			retryQueue.setCurrentUserId("user_123")
+
+			// Same user login - should not clear
+			let wasCleared = retryQueue.clearIfUserChanged("user_123")
+			expect(wasCleared).toBe(false)
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+
+			// Different user login - should clear
+			wasCleared = retryQueue.clearIfUserChanged("user_456")
+			expect(wasCleared).toBe(true)
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+			expect(retryQueue.getCurrentUserId()).toBe("user_456")
+		})
+
+		it("should clear queue on logout (undefined user)", async () => {
+			// Set initial user
+			retryQueue.setCurrentUserId("user_123")
+
+			// Add some requests
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+
+			// Logout (undefined user) - should clear
+			const wasCleared = retryQueue.clearIfUserChanged(undefined)
+			expect(wasCleared).toBe(true)
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+			expect(retryQueue.getCurrentUserId()).toBeUndefined()
+		})
+
+		it("should not clear on first login (no previous user)", async () => {
+			// Add some requests before any user is set
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+
+			// First login - should not clear
+			const wasCleared = retryQueue.clearIfUserChanged("user_123")
+			expect(wasCleared).toBe(false)
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(2)
+			expect(retryQueue.getCurrentUserId()).toBe("user_123")
+		})
+
+		it("should handle multiple user transitions correctly", async () => {
+			const clearListener = vi.fn()
+			retryQueue.on("queue-cleared", clearListener)
+
+			// First user logs in
+			retryQueue.clearIfUserChanged("user_123")
+			await retryQueue.enqueue("https://api.example.com/user1-req", { method: "POST" }, "telemetry")
+
+			// User logs out
+			const clearedOnLogout = retryQueue.clearIfUserChanged(undefined)
+			expect(clearedOnLogout).toBe(true)
+			expect(clearListener).toHaveBeenCalledTimes(1)
+
+			// Different user logs in
+			await retryQueue.enqueue("https://api.example.com/user2-req", { method: "POST" }, "telemetry")
+			const clearedOnNewUser = retryQueue.clearIfUserChanged("user_456")
+			expect(clearedOnNewUser).toBe(true)
+			expect(clearListener).toHaveBeenCalledTimes(2)
+
+			// Same user logs back in
+			await retryQueue.enqueue("https://api.example.com/user2-req2", { method: "POST" }, "telemetry")
+			const notCleared = retryQueue.clearIfUserChanged("user_456")
+			expect(notCleared).toBe(false)
+			expect(clearListener).toHaveBeenCalledTimes(2) // Still 2, not cleared
+
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1) // Only the last request remains
+		})
+	})
+
+	describe("retryAll", () => {
+		let fetchMock: ReturnType<typeof vi.fn>
+
+		beforeEach(() => {
+			// Mock global fetch
+			fetchMock = vi.fn()
+			global.fetch = fetchMock
+		})
+
+		afterEach(() => {
+			vi.restoreAllMocks()
+		})
+
+		it("should process requests in FIFO order", async () => {
+			const successListener = vi.fn()
+			retryQueue.on("request-retry-success", successListener)
+
+			// Add multiple requests
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
+
+			// Mock successful responses
+			fetchMock.mockResolvedValue({ ok: true })
+
+			await retryQueue.retryAll()
+
+			// Check that fetch was called in FIFO order
+			expect(fetchMock).toHaveBeenCalledTimes(3)
+			expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/test1")
+			expect(fetchMock.mock.calls[1]?.[0]).toBe("https://api.example.com/test2")
+			expect(fetchMock.mock.calls[2]?.[0]).toBe("https://api.example.com/test3")
+
+			// Check that success events were emitted
+			expect(successListener).toHaveBeenCalledTimes(3)
+
+			// Queue should be empty after successful retries
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+
+		it("should handle failed retries and increment retry count", async () => {
+			const failListener = vi.fn()
+			retryQueue.on("request-retry-failed", failListener)
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Mock failed response
+			fetchMock.mockRejectedValue(new Error("Network error"))
+
+			await retryQueue.retryAll()
+
+			// Check that failure event was emitted
+			expect(failListener).toHaveBeenCalledWith(
+				expect.objectContaining({
+					url: "https://api.example.com/test",
+					retryCount: 1,
+					lastError: "Network error",
+				}),
+				expect.any(Error),
+			)
+
+			// Request should still be in queue
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1)
+		})
+
+		it("should enforce max retries limit", async () => {
+			// Create queue with max retries of 2
+			retryQueue = new RetryQueue(mockContext, { maxRetries: 2 })
+
+			const maxRetriesListener = vi.fn()
+			retryQueue.on("request-max-retries-exceeded", maxRetriesListener)
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Mock failed responses
+			fetchMock.mockRejectedValue(new Error("Network error"))
+
+			// First retry
+			await retryQueue.retryAll()
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1) // Still in queue
+
+			// Second retry - should hit max retries
+			await retryQueue.retryAll()
+
+			// Check that max retries event was emitted
+			expect(maxRetriesListener).toHaveBeenCalledWith(
+				expect.objectContaining({
+					url: "https://api.example.com/test",
+					retryCount: 2,
+				}),
+				expect.any(Error),
+			)
+
+			// Request should be removed from queue after exceeding max retries
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+
+		it("should not process if already processing", async () => {
+			// Add a request
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Mock a slow response
+			fetchMock.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 100)))
+
+			// Start first retryAll (don't await)
+			const firstCall = retryQueue.retryAll()
+
+			// Try to call retryAll again immediately
+			const secondCall = retryQueue.retryAll()
+
+			// Both should complete without errors
+			await Promise.all([firstCall, secondCall])
+
+			// Fetch should only be called once (from the first call)
+			expect(fetchMock).toHaveBeenCalledTimes(1)
+		})
+
+		it("should handle empty queue gracefully", async () => {
+			// Call retryAll on empty queue
+			await expect(retryQueue.retryAll()).resolves.toBeUndefined()
+
+			// No fetch calls should be made
+			expect(fetchMock).not.toHaveBeenCalled()
+		})
+
+		it("should use auth header provider if available", async () => {
+			const authHeaderProvider = vi.fn().mockReturnValue({
+				Authorization: "Bearer fresh-token",
+			})
+
+			retryQueue = new RetryQueue(mockContext, {}, undefined, authHeaderProvider)
+
+			await retryQueue.enqueue(
+				"https://api.example.com/test",
+				{
+					method: "POST",
+					headers: { "Content-Type": "application/json" },
+				},
+				"telemetry",
+			)
+
+			fetchMock.mockResolvedValue({ ok: true })
+
+			await retryQueue.retryAll()
+
+			// Check that fresh auth headers were used
+			expect(fetchMock).toHaveBeenCalledWith(
+				"https://api.example.com/test",
+				expect.objectContaining({
+					headers: expect.objectContaining({
+						Authorization: "Bearer fresh-token",
+						"Content-Type": "application/json",
+						"X-Retry-Queue": "true",
+					}),
+				}),
+			)
+
+			expect(authHeaderProvider).toHaveBeenCalled()
+		})
+
+		it("should respect configurable timeout", async () => {
+			// Create queue with custom timeout (short timeout for testing)
+			retryQueue = new RetryQueue(mockContext, { requestTimeout: 100 })
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Mock fetch to reject with abort error
+			const abortError = new Error("The operation was aborted")
+			abortError.name = "AbortError"
+			fetchMock.mockRejectedValue(abortError)
+
+			const failListener = vi.fn()
+			retryQueue.on("request-retry-failed", failListener)
+
+			await retryQueue.retryAll()
+
+			// Check that the request failed with an abort error
+			expect(failListener).toHaveBeenCalledWith(
+				expect.objectContaining({
+					url: "https://api.example.com/test",
+					lastError: "The operation was aborted",
+				}),
+				expect.any(Error),
+			)
+
+			// The timeout configuration is being used (verified by the constructor accepting it)
+			// The actual timeout behavior is handled by the browser's AbortController
+		})
+
+		it("should retry on 500+ status codes", async () => {
+			const failListener = vi.fn()
+			const successListener = vi.fn()
+			retryQueue.on("request-retry-failed", failListener)
+			retryQueue.on("request-retry-success", successListener)
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// First attempt: 500 error
+			fetchMock.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error" })
+
+			await retryQueue.retryAll()
+
+			// Should fail and remain in queue
+			expect(failListener).toHaveBeenCalledWith(
+				expect.objectContaining({
+					url: "https://api.example.com/test",
+					retryCount: 1,
+					lastError: "Server error: 500 Internal Server Error",
+				}),
+				expect.any(Error),
+			)
+
+			let stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(1)
+
+			// Second attempt: success
+			fetchMock.mockResolvedValueOnce({ ok: true, status: 200 })
+
+			await retryQueue.retryAll()
+
+			// Should succeed and be removed from queue
+			expect(successListener).toHaveBeenCalled()
+			stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+
+		it("should pause entire queue on 429 rate limiting with Retry-After header", async () => {
+			// Add multiple requests to test queue-wide pause
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
+
+			// Mock 429 response with Retry-After header (in seconds) for the first request
+			const retryAfterResponse = {
+				ok: false,
+				status: 429,
+				headers: {
+					get: vi.fn((header: string) => {
+						if (header === "Retry-After") return "2" // 2 seconds
+						return null
+					}),
+				},
+			}
+
+			fetchMock.mockResolvedValueOnce(retryAfterResponse)
+
+			await retryQueue.retryAll()
+
+			// All requests should still be in queue
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(3)
+
+			// Only the first request should have been attempted
+			expect(fetchMock).toHaveBeenCalledTimes(1)
+			expect(fetchMock).toHaveBeenCalledWith("https://api.example.com/test1", expect.any(Object))
+
+			// Try to retry immediately - should be skipped due to queue-wide rate limiting
+			fetchMock.mockClear()
+			await retryQueue.retryAll()
+
+			// No fetch calls should be made because the entire queue is paused
+			expect(fetchMock).not.toHaveBeenCalled()
+		})
+
+		it("should process all requests after rate limit period expires", async () => {
+			// Add multiple requests
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+
+			// Mock 429 response with very short Retry-After (for testing)
+			const retryAfterResponse = {
+				ok: false,
+				status: 429,
+				headers: {
+					get: vi.fn((header: string) => {
+						if (header === "Retry-After") return "0" // 0 seconds (immediate)
+						return null
+					}),
+				},
+			}
+
+			fetchMock.mockResolvedValueOnce(retryAfterResponse)
+
+			await retryQueue.retryAll()
+
+			// Queue should be paused but requests still in queue
+			expect(retryQueue.getStats().totalQueued).toBe(2)
+
+			// Wait a tiny bit for the rate limit to "expire"
+			await new Promise((resolve) => setTimeout(resolve, 10))
+
+			// Mock successful responses for both requests
+			fetchMock.mockResolvedValue({ ok: true })
+
+			// Now retry should process all requests
+			await retryQueue.retryAll()
+
+			// All requests should be processed and removed from queue
+			expect(retryQueue.getStats().totalQueued).toBe(0)
+			// First request will be retried plus the second one
+			expect(fetchMock).toHaveBeenCalledTimes(3) // 1 (429) + 2 (success)
+		})
+
+		it("should not retry on 401/403 auth errors", async () => {
+			const successListener = vi.fn()
+			retryQueue.on("request-retry-success", successListener)
+
+			await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
+
+			// Mock 401 error
+			fetchMock.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" })
+
+			await retryQueue.retryAll()
+
+			// Should be removed from queue without retry (401 is a client error)
+			expect(successListener).toHaveBeenCalled()
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+
+			// Test 403 as well
+			await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
+			fetchMock.mockResolvedValueOnce({ ok: false, status: 403, statusText: "Forbidden" })
+
+			await retryQueue.retryAll()
+
+			// Should also be removed from queue without retry
+			expect(successListener).toHaveBeenCalledTimes(2)
+			const stats2 = retryQueue.getStats()
+			expect(stats2.totalQueued).toBe(0)
+		})
+
+		it("should not retry on 400/404/422 client errors", async () => {
+			const successListener = vi.fn()
+			retryQueue.on("request-retry-success", successListener)
+
+			// Test various 4xx errors that should not be retried
+			const clientErrors = [
+				{ status: 400, statusText: "Bad Request" },
+				{ status: 404, statusText: "Not Found" },
+				{ status: 422, statusText: "Unprocessable Entity" },
+			]
+
+			for (const error of clientErrors) {
+				await retryQueue.enqueue(
+					`https://api.example.com/test-${error.status}`,
+					{ method: "POST" },
+					"telemetry",
+				)
+				fetchMock.mockResolvedValueOnce({ ok: false, ...error })
+			}
+
+			await retryQueue.retryAll()
+
+			// All requests should be removed from queue without retry
+			expect(successListener).toHaveBeenCalledTimes(3)
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+
+		it("should prevent concurrent processing", async () => {
+			// Add a single request
+			await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
+
+			// Mock slow response
+			let resolveFirst: () => void
+			const firstPromise = new Promise<{ ok: boolean }>((resolve) => {
+				resolveFirst = () => resolve({ ok: true })
+			})
+
+			fetchMock.mockReturnValueOnce(firstPromise)
+
+			// Start first retryAll (don't await)
+			const firstCall = retryQueue.retryAll()
+
+			// Try to call retryAll again immediately - should return immediately without processing
+			const secondCall = retryQueue.retryAll()
+
+			// Second call should return immediately
+			await secondCall
+
+			// Fetch should only be called once (from first call)
+			expect(fetchMock).toHaveBeenCalledTimes(1)
+
+			// Resolve the promise
+			resolveFirst!()
+
+			// Wait for first call to complete
+			await firstCall
+
+			// Queue should be empty
+			const stats = retryQueue.getStats()
+			expect(stats.totalQueued).toBe(0)
+		})
+	})
+})

+ 2 - 0
packages/cloud/src/retry-queue/index.ts

@@ -0,0 +1,2 @@
+export { RetryQueue } from "./RetryQueue.js"
+export type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./types.js"

+ 36 - 0
packages/cloud/src/retry-queue/types.ts

@@ -0,0 +1,36 @@
+export interface QueuedRequest {
+	id: string
+	url: string
+	options: RequestInit
+	timestamp: number
+	retryCount: number
+	type: "api-call" | "telemetry" | "settings" | "other"
+	operation?: string
+	lastError?: string
+}
+
+export interface QueueStats {
+	totalQueued: number
+	byType: Record<string, number>
+	oldestRequest?: Date
+	newestRequest?: Date
+	totalRetries: number
+	failedRetries: number
+}
+
+export interface RetryQueueConfig {
+	maxRetries: number // 0 means unlimited
+	retryDelay: number
+	maxQueueSize: number // FIFO eviction when full
+	persistQueue: boolean
+	networkCheckInterval: number // milliseconds
+	requestTimeout: number // milliseconds for request timeout
+}
+
+export interface RetryQueueEvents {
+	"request-queued": [request: QueuedRequest]
+	"request-retry-success": [request: QueuedRequest]
+	"request-retry-failed": [request: QueuedRequest, error: Error]
+	"request-max-retries-exceeded": [request: QueuedRequest, error: Error]
+	"queue-cleared": []
+}

+ 1 - 1
packages/evals/README.md

@@ -95,7 +95,7 @@ By default, the evals system uses the following ports:
 
 - **PostgreSQL**: 5433 (external) → 5432 (internal)
 - **Redis**: 6380 (external) → 6379 (internal)
-- **Web Service**: 3446 (external) → 3000 (internal)
+- **Web Service**: 3446 (external) → 3446 (internal)
 
 These ports are configured to avoid conflicts with other services that might be running on the standard PostgreSQL (5432) and Redis (6379) ports.
 

+ 1 - 1
packages/evals/docker-compose.yml

@@ -52,7 +52,7 @@ services:
             context: ../../
             dockerfile: packages/evals/Dockerfile.web
         ports:
-            - "${EVALS_WEB_PORT:-3446}:3000"
+            - "${EVALS_WEB_PORT:-3446}:3446"
         environment:
             - HOST_EXECUTION_METHOD=docker
         volumes:

+ 2 - 2
packages/evals/scripts/setup.sh

@@ -377,7 +377,7 @@ fi
 
 echo -e "\n🚀 You're ready to rock and roll! \n"
 
-if ! nc -z localhost 3000; then
+if ! nc -z localhost 3446; then
   read -p "🌐 Would you like to start the evals web app? (Y/n): " start_evals
 
   if [[ "$start_evals" =~ ^[Yy]|^$ ]]; then
@@ -386,5 +386,5 @@ if ! nc -z localhost 3000; then
     echo "💡 You can start it anytime with 'pnpm --filter @roo-code/web-evals dev'."
   fi
 else
-  echo "👟 The evals web app is running at http://localhost:3000 (or http://localhost:3446 if using Docker)"
+  echo "👟 The evals web app is running at http://localhost:3446"
 fi

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

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

+ 173 - 0
packages/types/src/__tests__/cloud.test.ts

@@ -0,0 +1,173 @@
+// npx vitest run src/__tests__/cloud.test.ts
+
+import {
+	organizationFeaturesSchema,
+	organizationSettingsSchema,
+	type OrganizationFeatures,
+	type OrganizationSettings,
+} from "../cloud.js"
+
+describe("organizationFeaturesSchema", () => {
+	it("should validate empty object", () => {
+		const result = organizationFeaturesSchema.safeParse({})
+		expect(result.success).toBe(true)
+		expect(result.data).toEqual({})
+	})
+
+	it("should validate with roomoteControlEnabled as true", () => {
+		const input = { roomoteControlEnabled: true }
+		const result = organizationFeaturesSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data).toEqual(input)
+	})
+
+	it("should validate with roomoteControlEnabled as false", () => {
+		const input = { roomoteControlEnabled: false }
+		const result = organizationFeaturesSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data).toEqual(input)
+	})
+
+	it("should reject non-boolean roomoteControlEnabled", () => {
+		const input = { roomoteControlEnabled: "true" }
+		const result = organizationFeaturesSchema.safeParse(input)
+		expect(result.success).toBe(false)
+	})
+
+	it("should allow additional properties (for future extensibility)", () => {
+		const input = { roomoteControlEnabled: true, futureProperty: "test" }
+		const result = organizationFeaturesSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data?.roomoteControlEnabled).toBe(true)
+		// Note: Additional properties are stripped by Zod, which is expected behavior
+	})
+
+	it("should have correct TypeScript type", () => {
+		// Type-only test to ensure TypeScript compilation
+		const features: OrganizationFeatures = {
+			roomoteControlEnabled: true,
+		}
+		expect(features.roomoteControlEnabled).toBe(true)
+
+		const emptyFeatures: OrganizationFeatures = {}
+		expect(emptyFeatures.roomoteControlEnabled).toBeUndefined()
+	})
+})
+
+describe("organizationSettingsSchema with features", () => {
+	const validBaseSettings = {
+		version: 1,
+		defaultSettings: {},
+		allowList: {
+			allowAll: true,
+			providers: {},
+		},
+	}
+
+	it("should validate without features property", () => {
+		const result = organizationSettingsSchema.safeParse(validBaseSettings)
+		expect(result.success).toBe(true)
+		expect(result.data?.features).toBeUndefined()
+	})
+
+	it("should validate with empty features object", () => {
+		const input = {
+			...validBaseSettings,
+			features: {},
+		}
+		const result = organizationSettingsSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data?.features).toEqual({})
+	})
+
+	it("should validate with features.roomoteControlEnabled as true", () => {
+		const input = {
+			...validBaseSettings,
+			features: {
+				roomoteControlEnabled: true,
+			},
+		}
+		const result = organizationSettingsSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data?.features?.roomoteControlEnabled).toBe(true)
+	})
+
+	it("should validate with features.roomoteControlEnabled as false", () => {
+		const input = {
+			...validBaseSettings,
+			features: {
+				roomoteControlEnabled: false,
+			},
+		}
+		const result = organizationSettingsSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data?.features?.roomoteControlEnabled).toBe(false)
+	})
+
+	it("should reject invalid features object", () => {
+		const input = {
+			...validBaseSettings,
+			features: {
+				roomoteControlEnabled: "invalid",
+			},
+		}
+		const result = organizationSettingsSchema.safeParse(input)
+		expect(result.success).toBe(false)
+	})
+
+	it("should have correct TypeScript type for features", () => {
+		// Type-only test to ensure TypeScript compilation
+		const settings: OrganizationSettings = {
+			version: 1,
+			defaultSettings: {},
+			allowList: {
+				allowAll: true,
+				providers: {},
+			},
+			features: {
+				roomoteControlEnabled: true,
+			},
+		}
+		expect(settings.features?.roomoteControlEnabled).toBe(true)
+
+		const settingsWithoutFeatures: OrganizationSettings = {
+			version: 1,
+			defaultSettings: {},
+			allowList: {
+				allowAll: true,
+				providers: {},
+			},
+		}
+		expect(settingsWithoutFeatures.features).toBeUndefined()
+	})
+
+	it("should maintain all existing properties", () => {
+		const input = {
+			version: 1,
+			cloudSettings: {
+				recordTaskMessages: true,
+				enableTaskSharing: false,
+			},
+			defaultSettings: {},
+			allowList: {
+				allowAll: false,
+				providers: {
+					openai: {
+						allowAll: true,
+						models: ["gpt-4"],
+					},
+				},
+			},
+			features: {
+				roomoteControlEnabled: true,
+			},
+			hiddenMcps: ["test-mcp"],
+			hideMarketplaceMcps: true,
+			mcps: [],
+			providerProfiles: {},
+		}
+		const result = organizationSettingsSchema.safeParse(input)
+		expect(result.success).toBe(true)
+		expect(result.data).toEqual(input)
+	})
+})

+ 16 - 1
packages/types/src/cloud.ts

@@ -133,6 +133,16 @@ export const organizationCloudSettingsSchema = z.object({
 
 export type OrganizationCloudSettings = z.infer<typeof organizationCloudSettingsSchema>
 
+/**
+ * OrganizationFeatures
+ */
+
+export const organizationFeaturesSchema = z.object({
+	roomoteControlEnabled: z.boolean().optional(),
+})
+
+export type OrganizationFeatures = z.infer<typeof organizationFeaturesSchema>
+
 /**
  * OrganizationSettings
  */
@@ -142,6 +152,7 @@ export const organizationSettingsSchema = z.object({
 	cloudSettings: organizationCloudSettingsSchema.optional(),
 	defaultSettings: organizationDefaultSettingsSchema,
 	allowList: organizationAllowListSchema,
+	features: organizationFeaturesSchema.optional(),
 	hiddenMcps: z.array(z.string()).optional(),
 	hideMarketplaceMcps: z.boolean().optional(),
 	mcps: z.array(mcpMarketplaceItemSchema).optional(),
@@ -228,9 +239,10 @@ export interface AuthService extends EventEmitter<AuthServiceEvents> {
 	broadcast(): void
 
 	// Authentication methods
-	login(): Promise<void>
+	login(landingPageSlug?: string): Promise<void>
 	logout(): Promise<void>
 	handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise<void>
+	switchOrganization(organizationId: string | null): Promise<void>
 
 	// State methods
 	getState(): AuthState
@@ -242,6 +254,9 @@ export interface AuthService extends EventEmitter<AuthServiceEvents> {
 	getSessionToken(): string | undefined
 	getUserInfo(): CloudUserInfo | null
 	getStoredOrganizationId(): string | null
+
+	// Organization management
+	getOrganizationMemberships(): Promise<CloudOrganizationMembership[]>
 }
 
 /**

+ 22 - 0
packages/types/src/cookie-consent.ts

@@ -0,0 +1,22 @@
+/**
+ * Cookie consent constants and types
+ * Shared across all Roo Code repositories
+ */
+
+/**
+ * The name of the cookie that stores user's consent preference
+ * Used by react-cookie-consent library
+ */
+export const CONSENT_COOKIE_NAME = "roo-code-cookie-consent"
+
+/**
+ * Possible values for the consent cookie
+ */
+export type ConsentCookieValue = "true" | "false"
+
+/**
+ * Cookie consent event names for communication between components
+ */
+export const COOKIE_CONSENT_EVENTS = {
+	CHANGED: "cookieConsentChanged",
+} as const

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

@@ -164,6 +164,7 @@ export const globalSettingsSchema = z.object({
 	ghostServiceSettings: ghostServiceSettingsSchema, // kilocode_change
 	includeTaskHistoryInEnhance: z.boolean().optional(),
 	historyPreviewCollapsed: z.boolean().optional(),
+	reasoningBlockCollapsed: z.boolean().optional(),
 	profileThresholds: z.record(z.string(), z.number()).optional(),
 	hasOpenedModeSelector: z.boolean().optional(),
 	lastModeExportPath: z.string().optional(),

+ 1 - 0
packages/types/src/index.ts

@@ -1,6 +1,7 @@
 export * from "./api.js"
 export * from "./cloud.js"
 export * from "./codebase-index.js"
+export * from "./cookie-consent.js"
 export * from "./events.js"
 export * from "./experiment.js"
 export * from "./followup.js"

+ 1 - 0
packages/types/src/message.ts

@@ -96,6 +96,7 @@ export function isResumableAsk(ask: ClineAsk): ask is ResumableAsk {
  */
 
 export const interactiveAsks = [
+	"followup",
 	"command",
 	"tool",
 	"browser_action_launch",

+ 174 - 61
packages/types/src/provider-settings.ts

@@ -27,57 +27,132 @@ import {
 	internationalZAiModels,
 } from "./providers/index.js"
 
+/**
+ * constants
+ */
+
+export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
+
+/**
+ * DynamicProvider
+ *
+ * Dynamic provider requires external API calls in order to get the model list.
+ */
+
+export const dynamicProviders = [
+	"openrouter",
+	"vercel-ai-gateway",
+	"huggingface",
+	"litellm",
+	"kilocode-openrouter",
+	"deepinfra",
+	"io-intelligence",
+	"requesty",
+	"unbound",
+	"glama",
+] as const
+
+export type DynamicProvider = (typeof dynamicProviders)[number]
+
+export const isDynamicProvider = (key: string): key is DynamicProvider =>
+	dynamicProviders.includes(key as DynamicProvider)
+
+/**
+ * LocalProvider
+ *
+ * Local providers require localhost API calls in order to get the model list.
+ */
+
+export const localProviders = ["ollama", "lmstudio"] as const
+
+export type LocalProvider = (typeof localProviders)[number]
+
+export const isLocalProvider = (key: string): key is LocalProvider => localProviders.includes(key as LocalProvider)
+
+/**
+ * InternalProvider
+ *
+ * Internal providers require internal VSCode API calls in order to get the
+ * model list.
+ */
+
+export const internalProviders = ["vscode-lm"] as const
+
+export type InternalProvider = (typeof internalProviders)[number]
+
+export const isInternalProvider = (key: string): key is InternalProvider =>
+	internalProviders.includes(key as InternalProvider)
+
+/**
+ * CustomProvider
+ *
+ * Custom providers are completely configurable within Roo Code settings.
+ */
+
+export const customProviders = ["openai"] as const
+
+export type CustomProvider = (typeof customProviders)[number]
+
+export const isCustomProvider = (key: string): key is CustomProvider => customProviders.includes(key as CustomProvider)
+
+/**
+ * FauxProvider
+ *
+ * Faux providers do not make external inference calls and therefore do not have
+ * model lists.
+ */
+
+export const fauxProviders = ["fake-ai", "human-relay"] as const
+
+export type FauxProvider = (typeof fauxProviders)[number]
+
+export const isFauxProvider = (key: string): key is FauxProvider => fauxProviders.includes(key as FauxProvider)
+
 /**
  * ProviderName
  */
 
 export const providerNames = [
+	...dynamicProviders,
+	...localProviders,
+	...internalProviders,
+	...customProviders,
+	...fauxProviders,
 	"anthropic",
-	"claude-code",
-	"glama",
-	"openrouter",
 	"bedrock",
-	"vertex",
-	"openai",
-	"ollama",
-	"vscode-lm",
-	"lmstudio",
+	"cerebras",
+	"chutes",
+	"claude-code",
+	"doubao",
+	"deepseek",
+	"featherless",
+	"fireworks",
 	"gemini",
-	"openai-native",
+	"gemini-cli",
+	"groq",
 	"mistral",
 	"moonshot",
-	"deepseek",
-	"deepinfra",
-	"doubao",
+	"openai-native",
 	"qwen-code",
-	"unbound",
-	"requesty",
-	"human-relay",
-	"fake-ai",
-	"xai",
-	"groq",
-	"chutes",
-	"litellm",
+	"roo",
 	// kilocode_change start
 	"kilocode",
 	"gemini-cli",
 	"virtual-quota-fallback",
 	// kilocode_change end
-	"huggingface",
-	"cerebras",
 	"sambanova",
+	"vertex",
+	"xai",
 	"zai",
-	"fireworks",
-	"featherless",
-	"io-intelligence",
-	"roo",
-	"vercel-ai-gateway",
 ] as const
 
 export const providerNamesSchema = z.enum(providerNames)
 
 export type ProviderName = z.infer<typeof providerNamesSchema>
 
+export const isProviderName = (key: unknown): key is ProviderName =>
+	typeof key === "string" && providerNames.includes(key as ProviderName)
+
 /**
  * ProviderSettingsEntry
  */
@@ -95,11 +170,6 @@ export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
  * ProviderSettings
  */
 
-/**
- * Default value for consecutive mistake limit
- */
-export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
-
 const baseProviderSettingsSchema = z.object({
 	includeMaxTokens: z.boolean().optional(),
 	diffEnabled: z.boolean().optional(),
@@ -128,7 +198,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
 	apiKey: z.string().optional(),
 	anthropicBaseUrl: z.string().optional(),
 	anthropicUseAuthToken: z.boolean().optional(),
-	anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window
+	anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
 })
 
 const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
@@ -173,7 +243,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
 	awsModelContextWindow: z.number().optional(),
 	awsBedrockEndpointEnabled: z.boolean().optional(),
 	awsBedrockEndpoint: z.string().optional(),
-	awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window
+	awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
 })
 
 const vertexSchema = apiModelIdProviderModelSchema.extend({
@@ -203,6 +273,7 @@ const ollamaSchema = baseProviderSettingsSchema.extend({
 	ollamaModelId: z.string().optional(),
 	ollamaBaseUrl: z.string().optional(),
 	ollamaApiKey: z.string().optional(),
+	ollamaNumCtx: z.number().int().min(128).optional(),
 })
 
 const vsCodeLmSchema = baseProviderSettingsSchema.extend({
@@ -356,11 +427,10 @@ const virtualQuotaFallbackSchema = baseProviderSettingsSchema.extend({
 export const zaiApiLineSchema = z.enum(["international_coding", "international", "china_coding", "china"])
 
 export type ZaiApiLine = z.infer<typeof zaiApiLineSchema>
-// kilocode_change end
 
 const zaiSchema = apiModelIdProviderModelSchema.extend({
 	zaiApiKey: z.string().optional(),
-	zaiApiLine: zaiApiLineSchema.optional(), // kilocode_change
+	zaiApiLine: zaiApiLineSchema.optional(),
 })
 
 const fireworksSchema = apiModelIdProviderModelSchema.extend({
@@ -381,7 +451,7 @@ const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
 })
 
 const rooSchema = apiModelIdProviderModelSchema.extend({
-	// No additional fields needed - uses cloud authentication
+	// No additional fields needed - uses cloud authentication.
 })
 
 const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
@@ -494,7 +564,11 @@ export type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema
 
 export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options
 
-export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
+/**
+ * ModelIdKey
+ */
+
+export const modelIdKeys = [
 	"apiModelId",
 	"glamaModelId",
 	"openRouterModelId",
@@ -509,13 +583,67 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
 	"ioIntelligenceModelId",
 	"vercelAiGatewayModelId",
 	"deepInfraModelId",
-]
+	"kilocodeModel",
+] as const satisfies readonly (keyof ProviderSettings)[]
+
+export type ModelIdKey = (typeof modelIdKeys)[number]
 
 export const getModelId = (settings: ProviderSettings): string | undefined => {
-	const modelIdKey = MODEL_ID_KEYS.find((key) => settings[key])
-	return modelIdKey ? (settings[modelIdKey] as string) : undefined
+	const modelIdKey = modelIdKeys.find((key) => settings[key])
+	return modelIdKey ? settings[modelIdKey] : undefined
 }
 
+/**
+ * TypicalProvider
+ */
+
+export type TypicalProvider = Exclude<ProviderName, InternalProvider | CustomProvider | FauxProvider>
+
+export const isTypicalProvider = (key: unknown): key is TypicalProvider =>
+	isProviderName(key) && !isInternalProvider(key) && !isCustomProvider(key) && !isFauxProvider(key)
+
+export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
+	anthropic: "apiModelId",
+	"claude-code": "apiModelId",
+	glama: "glamaModelId",
+	openrouter: "openRouterModelId",
+	"kilocode-openrouter": "openRouterModelId",
+	bedrock: "apiModelId",
+	vertex: "apiModelId",
+	"openai-native": "openAiModelId",
+	ollama: "ollamaModelId",
+	lmstudio: "lmStudioModelId",
+	gemini: "apiModelId",
+	"gemini-cli": "apiModelId",
+	mistral: "apiModelId",
+	moonshot: "apiModelId",
+	deepseek: "apiModelId",
+	deepinfra: "deepInfraModelId",
+	doubao: "apiModelId",
+	"qwen-code": "apiModelId",
+	unbound: "unboundModelId",
+	requesty: "requestyModelId",
+	xai: "apiModelId",
+	groq: "apiModelId",
+	chutes: "apiModelId",
+	litellm: "litellmModelId",
+	huggingface: "huggingFaceModelId",
+	cerebras: "apiModelId",
+	sambanova: "apiModelId",
+	zai: "apiModelId",
+	fireworks: "apiModelId",
+	featherless: "apiModelId",
+	"io-intelligence": "ioIntelligenceModelId",
+	roo: "apiModelId",
+	"vercel-ai-gateway": "vercelAiGatewayModelId",
+	kilocode: "kilocodeModel",
+	"virtual-quota-fallback": "apiModelId",
+}
+
+/**
+ * ANTHROPIC_STYLE_PROVIDERS
+ */
+
 // Providers that use Anthropic-style API protocol.
 export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"]
 
@@ -536,6 +664,10 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
 	return "openai"
 }
 
+/**
+ * MODELS_BY_PROVIDER
+ */
+
 export const MODELS_BY_PROVIDER: Record<
 	Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama">,
 	{ id: ProviderName; label: string; models: string[] }
@@ -633,28 +765,9 @@ export const MODELS_BY_PROVIDER: Record<
 
 	// kilocode_change start
 	kilocode: { id: "kilocode", label: "Kilocode", models: [] },
+	"kilocode-openrouter": { id: "kilocode-openrouter", label: "Kilocode", models: [] }, // temporarily needed to satisfy because we're using 2 inconsistent names apparently
 	"virtual-quota-fallback": { id: "virtual-quota-fallback", label: "Virtual Quota Fallback", models: [] },
 	// kilocode_change end
 	deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
 	"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
 }
-
-export const dynamicProviders = [
-	"glama",
-	"huggingface",
-	"litellm",
-	"openrouter",
-	"requesty",
-	"unbound",
-	// kilocode_change start
-	"kilocode",
-	"virtual-quota-fallback",
-	// kilocode_change end
-	"deepinfra",
-	"vercel-ai-gateway",
-] as const satisfies readonly ProviderName[]
-
-export type DynamicProvider = (typeof dynamicProviders)[number]
-
-export const isDynamicProvider = (key: string): key is DynamicProvider =>
-	dynamicProviders.includes(key as DynamicProvider)

+ 5 - 5
packages/types/src/providers/chutes.ts

@@ -35,9 +35,9 @@ export type ChutesModelId =
 	| "zai-org/GLM-4.5-Air"
 	| "zai-org/GLM-4.5-FP8"
 	// kilocode_change start
-	| "zai-org/GLM-4.5-turbo"
 	| "zai-org/GLM-4.5V"
 	// kilocode_change end
+	| "zai-org/GLM-4.5-turbo"
 	| "moonshotai/Kimi-K2-Instruct-75k"
 	| "moonshotai/Kimi-K2-Instruct-0905"
 	// kilocode_change start
@@ -316,16 +316,16 @@ export const chutesModels = {
 		description:
 			"GLM-4.5-FP8 model with 128k token context window, optimized for agent-based applications with MoE architecture.",
 	},
-	// kilocode_change start
 	"zai-org/GLM-4.5-turbo": {
 		maxTokens: 32768,
 		contextWindow: 131072,
 		supportsImages: false,
 		supportsPromptCache: false,
-		inputPrice: 1.0,
-		outputPrice: 3.0,
-		description: "GLM-4.5-Turbo model.",
+		inputPrice: 1,
+		outputPrice: 3,
+		description: "GLM-4.5-turbo model with 128K token context window, optimized for fast inference.",
 	},
+	// kilocode_change start
 	"zai-org/GLM-4.5V": {
 		maxTokens: 32768,
 		contextWindow: 131072,

+ 11 - 2
packages/types/src/providers/roo.ts

@@ -1,7 +1,6 @@
 import type { ModelInfo } from "../model.js"
 
-// Roo provider with single model
-export type RooModelId = "xai/grok-code-fast-1"
+export type RooModelId = "xai/grok-code-fast-1" | "roo/code-supernova"
 
 export const rooDefaultModelId: RooModelId = "xai/grok-code-fast-1"
 
@@ -16,4 +15,14 @@ export const rooModels = {
 		description:
 			"A reasoning model that is blazing fast and excels at agentic coding, accessible for free through Roo Code Cloud for a limited time. (Note: the free prompts and completions are logged by xAI and used to improve the model.)",
 	},
+	"roo/code-supernova": {
+		maxTokens: 16_384,
+		contextWindow: 200_000,
+		supportsImages: true,
+		supportsPromptCache: true,
+		inputPrice: 0,
+		outputPrice: 0,
+		description:
+			"A versatile agentic coding stealth model that supports image inputs, accessible for free through Roo Code Cloud for a limited time. (Note: the free prompts and completions are logged by the model provider and used to improve the model.)",
+	},
 } as const satisfies Record<string, ModelInfo>

+ 1 - 3
packages/types/src/providers/zai.ts

@@ -1,5 +1,5 @@
 import type { ModelInfo } from "../model.js"
-import { ZaiApiLine } from "../provider-settings.js" // kilocode_change
+import { ZaiApiLine } from "../provider-settings.js"
 
 // Z AI
 // https://docs.z.ai/guides/llm/glm-4.5
@@ -127,7 +127,6 @@ export const mainlandZAiModels = {
 
 export const ZAI_DEFAULT_TEMPERATURE = 0
 
-// kilocode_change start
 export const zaiApiLineConfigs = {
 	international_coding: {
 		name: "International Coding Plan",
@@ -138,4 +137,3 @@ export const zaiApiLineConfigs = {
 	china_coding: { name: "China Coding Plan", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", isChina: true },
 	china: { name: "China Standard", baseUrl: "https://open.bigmodel.cn/api/paas/v4", isChina: true },
 } satisfies Record<ZaiApiLine, { name: string; baseUrl: string; isChina: boolean }>
-// kilocode_change end

+ 1 - 1
packages/types/src/single-file-read-models.ts

@@ -11,5 +11,5 @@
  */
 export function shouldUseSingleFileRead(modelId: string): boolean {
 	return false // kilocode_change
-	return modelId.includes("grok-code-fast-1")
+	return modelId.includes("grok-code-fast-1") || modelId.includes("code-supernova")
 }

+ 1 - 0
packages/types/src/vscode.ts

@@ -62,6 +62,7 @@ export const commandIds = [
 	"generateTerminalCommand", // kilocode_change
 	"handleExternalUri", // kilocode_change - for JetBrains plugin URL forwarding
 	"focusPanel",
+	"toggleAutoApprove",
 ] as const
 
 export type CommandId = (typeof commandIds)[number]

+ 25 - 8
pnpm-lock.yaml

@@ -508,6 +508,9 @@ importers:
       react:
         specifier: ^18.3.1
         version: 18.3.1
+      react-cookie-consent:
+        specifier: ^9.0.0
+        version: 9.0.0([email protected])
       react-dom:
         specifier: ^18.3.1
         version: 18.3.1([email protected])
@@ -523,6 +526,9 @@ importers:
       tailwindcss-animate:
         specifier: ^1.0.7
         version: 1.0.7([email protected]([email protected](@swc/[email protected])(@types/[email protected])([email protected])))
+      tldts:
+        specifier: ^6.1.86
+        version: 6.1.86
       zod:
         specifier: ^3.25.61
         version: 3.25.61
@@ -1305,8 +1311,8 @@ importers:
         specifier: ^0.5.0
         version: 0.5.0
       axios:
-        specifier: ^1.7.4
-        version: 1.9.0
+        specifier: ^1.12.0
+        version: 1.12.2
       cheerio:
         specifier: ^1.0.0
         version: 1.0.0
@@ -1684,8 +1690,8 @@ importers:
         specifier: ^1.4.0
         version: 1.4.0([email protected])
       axios:
-        specifier: ^1.7.4
-        version: 1.9.0
+        specifier: ^1.12.0
+        version: 1.12.2
       class-variance-authority:
         specifier: ^0.7.1
         version: 0.7.1
@@ -8062,8 +8068,8 @@ packages:
     resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
     engines: {node: '>= 0.4'}
 
-  axios@1.9.0:
-    resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
+  axios@1.12.2:
+    resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
 
   [email protected]:
     resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==}
@@ -15282,6 +15288,12 @@ packages:
     resolution: {integrity: sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg==}
     engines: {node: '>= 14.0.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-Blyj+m+Zz7SFHYqT18p16EANgnSg2sIyU6Yp3vk83AnOnSW7qnehPkUe4+8+qxztJrNmCH5GP+VHsWzAKVOoZA==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      react: '>=16'
+
   [email protected]:
     resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==}
     peerDependencies:
@@ -26524,7 +26536,7 @@ snapshots:
     dependencies:
       possible-typed-array-names: 1.1.0
 
-  axios@1.9.0:
+  axios@1.12.2:
     dependencies:
       follow-redirects: 1.15.9([email protected])
       form-data: 4.0.4
@@ -35308,6 +35320,11 @@ snapshots:
     dependencies:
       cross-spawn-windows-exe: 1.2.0
 
+  [email protected]([email protected]):
+    dependencies:
+      js-cookie: 2.2.1
+      react: 18.3.1
+
   [email protected]([email protected]):
     dependencies:
       countup.js: 2.9.0
@@ -38488,7 +38505,7 @@ snapshots:
 
   [email protected]:
     dependencies:
-      axios: 1.9.0
+      axios: 1.12.2
       joi: 17.13.3
       lodash: 4.17.21
       minimist: 1.2.8

BIN
releases/3.28.3-release.png


BIN
releases/3.28.4-release.png


BIN
releases/3.28.5-release.png


BIN
releases/3.28.6-release.png


BIN
releases/3.28.7-release.png


+ 217 - 64
scripts/find-missing-translations.js

@@ -7,12 +7,16 @@
  * Options:
  *   --locale=<locale>   Only check a specific locale (e.g. --locale=fr)
  *   --file=<file>       Only check a specific file (e.g. --file=chat.json)
- *   --area=<area>       Only check a specific area (core, webview, or both)
+ *   --area=<area>       Only check a specific area (core, webview, package-nls, or all)
  *   --help              Show this help message
  */
 
-const fs = require("fs")
 const path = require("path")
+const { promises: fs } = require("fs")
+
+const readFile = fs.readFile
+const readdir = fs.readdir
+const stat = fs.stat
 
 // Process command line arguments
 const args = process.argv.slice(2).reduce(
@@ -26,15 +30,15 @@ const args = process.argv.slice(2).reduce(
 		} else if (arg.startsWith("--area=")) {
 			acc.area = arg.split("=")[1]
 			// Validate area value
-			if (!["core", "webview", "both"].includes(acc.area)) {
-				console.error(`Error: Invalid area '${acc.area}'. Must be 'core', 'webview', or 'both'.`)
+			if (!["core", "webview", "package-nls", "all"].includes(acc.area)) {
+				console.error(`Error: Invalid area '${acc.area}'. Must be 'core', 'webview', 'package-nls', or 'all'.`)
 				process.exit(1)
 			}
 		}
 		return acc
 	},
-	{ area: "both" },
-) // Default to checking both areas
+	{ area: "all" },
+) // Default to checking all areas
 
 // Show help if requested
 if (args.help) {
@@ -50,10 +54,11 @@ Usage:
 Options:
   --locale=<locale>   Only check a specific locale (e.g. --locale=fr)
   --file=<file>       Only check a specific file (e.g. --file=chat.json)
-  --area=<area>       Only check a specific area (core, webview, or both)
+  --area=<area>       Only check a specific area (core, webview, package-nls, or all)
                       'core' = Backend (src/i18n/locales)
                       'webview' = Frontend UI (webview-ui/src/i18n/locales)
-                      'both' = Check both areas (default)
+                      'package-nls' = VSCode package.nls.json files
+                      'all' = Check all areas (default)
   --help              Show this help message
 
 Output:
@@ -69,7 +74,7 @@ const LOCALES_DIRS = {
 }
 
 // Determine which areas to check based on args
-const areasToCheck = args.area === "both" ? ["core", "webview"] : [args.area]
+const areasToCheck = args.area === "all" ? ["core", "webview", "package-nls"] : [args.area]
 
 // Recursively find all keys in an object
 function findKeys(obj, parentKey = "") {
@@ -105,18 +110,45 @@ function getValueAtPath(obj, path) {
 	return current
 }
 
+// Shared utility to safely parse JSON files with error handling
+async function parseJsonFile(filePath) {
+	try {
+		const content = await readFile(filePath, "utf8")
+		return JSON.parse(content)
+	} catch (error) {
+		if (error.code === "ENOENT") {
+			return null // File doesn't exist
+		}
+		throw new Error(`Error parsing JSON file '${filePath}': ${error.message}`)
+	}
+}
+
+// Validate that a JSON object has a flat structure (no nested objects)
+function validateFlatStructure(obj, filePath) {
+	for (const [key, value] of Object.entries(obj)) {
+		if (typeof value === "object" && value !== null) {
+			console.error(`Error: ${filePath} should be a flat JSON structure. Found nested object at key '${key}'`)
+			process.exit(1)
+		}
+	}
+}
+
 // Function to check translations for a specific area
-function checkAreaTranslations(area) {
+async function checkAreaTranslations(area) {
 	const LOCALES_DIR = LOCALES_DIRS[area]
 
 	// Get all locale directories (or filter to the specified locale)
-	const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
-		const stats = fs.statSync(path.join(LOCALES_DIR, item))
-		return stats.isDirectory() && item !== "en" // Exclude English as it's our source
-	})
+	const dirContents = await readdir(LOCALES_DIR)
+	const allLocales = await Promise.all(
+		dirContents.map(async (item) => {
+			const stats = await stat(path.join(LOCALES_DIR, item))
+			return stats.isDirectory() && item !== "en" ? item : null
+		}),
+	)
+	const filteredLocales = allLocales.filter(Boolean)
 
 	// Filter to the specified locale if provided
-	const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
+	const locales = args.locale ? filteredLocales.filter((locale) => locale === args.locale) : filteredLocales
 
 	if (args.locale && locales.length === 0) {
 		console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
@@ -129,7 +161,8 @@ function checkAreaTranslations(area) {
 
 	// Get all English JSON files
 	const englishDir = path.join(LOCALES_DIR, "en")
-	let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))
+	const englishDirContents = await readdir(englishDir)
+	let englishFiles = englishDirContents.filter((file) => file.endsWith(".json") && !file.startsWith("."))
 
 	// Filter to the specified file if provided
 	if (args.file) {
@@ -140,81 +173,201 @@ function checkAreaTranslations(area) {
 		englishFiles = englishFiles.filter((file) => file === args.file)
 	}
 
-	// Load file contents
-	let englishFileContents
-
-	try {
-		englishFileContents = englishFiles.map((file) => ({
-			name: file,
-			content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
-		}))
-	} catch (e) {
-		console.error(`Error: File '${englishDir}' is not a valid JSON file`)
-		process.exit(1)
-	}
+	// Load file contents in parallel
+	const englishFileContents = await Promise.all(
+		englishFiles.map(async (file) => {
+			const filePath = path.join(englishDir, file)
+			const content = await parseJsonFile(filePath)
+			if (!content) {
+				console.error(`Error: Could not read file '${filePath}'`)
+				process.exit(1)
+			}
+			return { name: file, content }
+		}),
+	)
 
 	console.log(
 		`Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`,
 	)
 
+	// Precompute English keys per file
+	const englishFileKeys = new Map(englishFileContents.map((f) => [f.name, findKeys(f.content)]))
+
 	// Results object to store missing translations
 	const missingTranslations = {}
 
-	// For each locale, check for missing translations
-	for (const locale of locales) {
-		missingTranslations[locale] = {}
+	// Process all locales in parallel
+	await Promise.all(
+		locales.map(async (locale) => {
+			missingTranslations[locale] = {}
+
+			// Process all files for this locale in parallel
+			await Promise.all(
+				englishFileContents.map(async ({ name, content: englishContent }) => {
+					const localeFilePath = path.join(LOCALES_DIR, locale, name)
+
+					// Check if the file exists in the locale
+					const localeContent = await parseJsonFile(localeFilePath)
+					if (!localeContent) {
+						missingTranslations[locale][name] = { file: "File is missing entirely" }
+						return
+					}
+
+					// Find all keys in the English file
+					const englishKeys = englishFileKeys.get(name) || []
+
+					// Check for missing keys in the locale file
+					const missingKeys = []
+
+					for (const key of englishKeys) {
+						const englishValue = getValueAtPath(englishContent, key)
+						const localeValue = getValueAtPath(localeContent, key)
+
+						if (localeValue === undefined) {
+							missingKeys.push({
+								key,
+								englishValue,
+							})
+						}
+					}
+
+					if (missingKeys.length > 0) {
+						missingTranslations[locale][name] = missingKeys
+					}
+				}),
+			)
+		}),
+	)
+
+	return { missingTranslations, hasMissingTranslations: outputResults(missingTranslations, area) }
+}
+
+// Function to output results for an area
+function outputResults(missingTranslations, area) {
+	let hasMissingTranslations = false
+
+	console.log(`\n${area === "core" ? "BACKEND" : "FRONTEND"} Missing Translations Report:\n`)
+
+	for (const [locale, files] of Object.entries(missingTranslations)) {
+		if (Object.keys(files).length === 0) {
+			console.log(`✅ ${locale}: No missing translations`)
+			continue
+		}
 
-		for (const { name, content: englishContent } of englishFileContents) {
-			const localeFilePath = path.join(LOCALES_DIR, locale, name)
+		hasMissingTranslations = true
+		console.log(`📝 ${locale}:`)
 
-			// Check if the file exists in the locale
-			if (!fs.existsSync(localeFilePath)) {
-				missingTranslations[locale][name] = { file: "File is missing entirely" }
+		for (const [fileName, missingItems] of Object.entries(files)) {
+			if (missingItems.file) {
+				console.log(`  - ${fileName}: ${missingItems.file}`)
 				continue
 			}
 
-			// Load the locale file
-			let localeContent
+			console.log(`  - ${fileName}: ${missingItems.length} missing translations`)
+
+			for (const { key, englishValue } of missingItems) {
+				console.log(`      ${key}: "${englishValue}"`)
+			}
+		}
+
+		console.log("")
+	}
+
+	return hasMissingTranslations
+}
+
+// Function to check package.nls.json translations
+async function checkPackageNlsTranslations() {
+	const SRC_DIR = path.join(__dirname, "../src")
+
+	// Read the base package.nls.json file
+	const baseFilePath = path.join(SRC_DIR, "package.nls.json")
+	const baseContent = await parseJsonFile(baseFilePath)
+
+	if (!baseContent) {
+		console.warn(`Warning: Base package.nls.json not found at ${baseFilePath} - skipping package.nls checks`)
+		return { missingTranslations: {}, hasMissingTranslations: false }
+	}
+
+	// Validate that the base file has a flat structure
+	validateFlatStructure(baseContent, baseFilePath)
+
+	// Get all package.nls.*.json files
+	const srcDirContents = await readdir(SRC_DIR)
+	const nlsFiles = srcDirContents
+		.filter((file) => file.startsWith("package.nls.") && file.endsWith(".json"))
+		.filter((file) => file !== "package.nls.json") // Exclude the base file
+
+	// Filter to the specified locale if provided
+	const filesToCheck = args.locale
+		? nlsFiles.filter((file) => {
+				const locale = file.replace("package.nls.", "").replace(".json", "")
+				return locale === args.locale
+			})
+		: nlsFiles
+
+	if (args.locale && filesToCheck.length === 0) {
+		console.error(`Error: Locale '${args.locale}' not found in package.nls files`)
+		process.exit(1)
+	}
 
-			try {
-				localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
-			} catch (e) {
-				console.error(`Error: File '${localeFilePath}' is not a valid JSON file`)
+	console.log(
+		`\nPACKAGE.NLS - Checking ${filesToCheck.length} locale file(s): ${filesToCheck.map((f) => f.replace("package.nls.", "").replace(".json", "")).join(", ")}`,
+	)
+	console.log(`Checking against base package.nls.json with ${Object.keys(baseContent).length} keys`)
+
+	// Results object to store missing translations
+	const missingTranslations = {}
+
+	// Get all keys from the base file (package.nls files are flat, not nested)
+	const baseKeys = Object.keys(baseContent)
+
+	// Process all locale files in parallel
+	await Promise.all(
+		filesToCheck.map(async (file) => {
+			const locale = file.replace("package.nls.", "").replace(".json", "")
+			const localeFilePath = path.join(SRC_DIR, file)
+
+			const localeContent = await parseJsonFile(localeFilePath)
+			if (!localeContent) {
+				console.error(`Error: Could not read file '${localeFilePath}'`)
 				process.exit(1)
 			}
 
-			// Find all keys in the English file
-			const englishKeys = findKeys(englishContent)
+			// Validate that the locale file has a flat structure
+			validateFlatStructure(localeContent, localeFilePath)
 
-			// Check for missing keys in the locale file
+			// Check for missing keys
 			const missingKeys = []
 
-			for (const key of englishKeys) {
-				const englishValue = getValueAtPath(englishContent, key)
-				const localeValue = getValueAtPath(localeContent, key)
+			for (const key of baseKeys) {
+				const baseValue = baseContent[key]
+				const localeValue = localeContent[key]
 
 				if (localeValue === undefined) {
 					missingKeys.push({
 						key,
-						englishValue,
+						englishValue: baseValue,
 					})
 				}
 			}
 
 			if (missingKeys.length > 0) {
-				missingTranslations[locale][name] = missingKeys
+				missingTranslations[locale] = {
+					"package.nls.json": missingKeys,
+				}
 			}
-		}
-	}
+		}),
+	)
 
-	return { missingTranslations, hasMissingTranslations: outputResults(missingTranslations, area) }
+	return { missingTranslations, hasMissingTranslations: outputPackageNlsResults(missingTranslations) }
 }
 
-// Function to output results for an area
-function outputResults(missingTranslations, area) {
+// Function to output package.nls results
+function outputPackageNlsResults(missingTranslations) {
 	let hasMissingTranslations = false
 
-	console.log(`\n${area === "core" ? "BACKEND" : "FRONTEND"} Missing Translations Report:\n`)
+	console.log(`\nPACKAGE.NLS Missing Translations Report:\n`)
 
 	for (const [locale, files] of Object.entries(missingTranslations)) {
 		if (Object.keys(files).length === 0) {
@@ -226,11 +379,6 @@ function outputResults(missingTranslations, area) {
 		console.log(`📝 ${locale}:`)
 
 		for (const [fileName, missingItems] of Object.entries(files)) {
-			if (missingItems.file) {
-				console.log(`  - ${fileName}: ${missingItems.file}`)
-				continue
-			}
-
 			console.log(`  - ${fileName}: ${missingItems.length} missing translations`)
 
 			for (const { key, englishValue } of missingItems) {
@@ -245,7 +393,7 @@ function outputResults(missingTranslations, area) {
 }
 
 // Main function to find missing translations
-function findMissingTranslations() {
+async function findMissingTranslations() {
 	try {
 		console.log("Starting translation check...")
 
@@ -253,8 +401,13 @@ function findMissingTranslations() {
 
 		// Check each requested area
 		for (const area of areasToCheck) {
-			const { hasMissingTranslations } = checkAreaTranslations(area)
-			anyAreaMissingTranslations = anyAreaMissingTranslations || hasMissingTranslations
+			if (area === "package-nls") {
+				const { hasMissingTranslations } = await checkPackageNlsTranslations()
+				anyAreaMissingTranslations = anyAreaMissingTranslations || hasMissingTranslations
+			} else {
+				const { hasMissingTranslations } = await checkAreaTranslations(area)
+				anyAreaMissingTranslations = anyAreaMissingTranslations || hasMissingTranslations
+			}
 		}
 
 		// Summary

+ 12 - 0
src/activate/registerCommands.ts

@@ -277,6 +277,18 @@ const getCommandsMap = ({ context, outputChannel }: RegisterCommandOptions): Rec
 		}
 	},
 	// kilocode_change end
+	toggleAutoApprove: async () => {
+		const visibleProvider = getVisibleProviderOrLog(outputChannel)
+
+		if (!visibleProvider) {
+			return
+		}
+
+		visibleProvider.postMessageToWebview({
+			type: "action",
+			action: "toggleAutoApprove",
+		})
+	},
 })
 
 export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {

+ 2 - 0
src/api/index.ts

@@ -101,6 +101,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 		// kilocode_change start
 		case "kilocode":
 			return new KilocodeOpenrouterHandler(options)
+		case "kilocode-openrouter": // temp typing fix
+			return new KilocodeOpenrouterHandler(options)
 		case "gemini-cli":
 			return new GeminiCliHandler(options)
 		case "virtual-quota-fallback":

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

@@ -253,6 +253,28 @@ describe("ChutesHandler", () => {
 		)
 	})
 
+	it("should return zai-org/GLM-4.5-turbo model with correct configuration", () => {
+		const testModelId: ChutesModelId = "zai-org/GLM-4.5-turbo"
+		const handlerWithModel = new ChutesHandler({
+			apiModelId: testModelId,
+			chutesApiKey: "test-chutes-api-key",
+		})
+		const model = handlerWithModel.getModel()
+		expect(model.id).toBe(testModelId)
+		expect(model.info).toEqual(
+			expect.objectContaining({
+				maxTokens: 32768,
+				contextWindow: 131072,
+				supportsImages: false,
+				supportsPromptCache: false,
+				inputPrice: 1,
+				outputPrice: 3,
+				description: "GLM-4.5-turbo model with 128K token context window, optimized for fast inference.",
+				temperature: 0.5, // Default temperature for non-DeepSeek models
+			}),
+		)
+	})
+
 	it("should return Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8 model with correct configuration", () => {
 		const testModelId: ChutesModelId = "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8"
 		const handlerWithModel = new ChutesHandler({

+ 98 - 0
src/api/providers/__tests__/native-ollama.spec.ts

@@ -73,6 +73,61 @@ describe("NativeOllamaHandler", () => {
 			expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 })
 		})
 
+		it("should not include num_ctx by default", async () => {
+			// Mock the chat response
+			mockChat.mockImplementation(async function* () {
+				yield { message: { content: "Response" } }
+			})
+
+			const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
+
+			// Consume the stream
+			for await (const _ of stream) {
+				// consume stream
+			}
+
+			// Verify that num_ctx was NOT included in the options
+			expect(mockChat).toHaveBeenCalledWith(
+				expect.objectContaining({
+					options: expect.not.objectContaining({
+						num_ctx: expect.anything(),
+					}),
+				}),
+			)
+		})
+
+		it("should include num_ctx when explicitly set via ollamaNumCtx", async () => {
+			const options: ApiHandlerOptions = {
+				apiModelId: "llama2",
+				ollamaModelId: "llama2",
+				ollamaBaseUrl: "http://localhost:11434",
+				ollamaNumCtx: 8192, // Explicitly set num_ctx
+			}
+
+			handler = new NativeOllamaHandler(options)
+
+			// Mock the chat response
+			mockChat.mockImplementation(async function* () {
+				yield { message: { content: "Response" } }
+			})
+
+			const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
+
+			// Consume the stream
+			for await (const _ of stream) {
+				// consume stream
+			}
+
+			// Verify that num_ctx was included with the specified value
+			expect(mockChat).toHaveBeenCalledWith(
+				expect.objectContaining({
+					options: expect.objectContaining({
+						num_ctx: 8192,
+					}),
+				}),
+			)
+		})
+
 		// kilocode_change: skip, model is not guaranteed to exist
 		it.skip("should handle DeepSeek R1 models with reasoning detection", async () => {
 			const options: ApiHandlerOptions = {
@@ -121,6 +176,49 @@ describe("NativeOllamaHandler", () => {
 			})
 			expect(result).toBe("This is the response")
 		})
+
+		it("should not include num_ctx in completePrompt by default", async () => {
+			mockChat.mockResolvedValue({
+				message: { content: "Response" },
+			})
+
+			await handler.completePrompt("Test prompt")
+
+			// Verify that num_ctx was NOT included in the options
+			expect(mockChat).toHaveBeenCalledWith(
+				expect.objectContaining({
+					options: expect.not.objectContaining({
+						num_ctx: expect.anything(),
+					}),
+				}),
+			)
+		})
+
+		it("should include num_ctx in completePrompt when explicitly set", async () => {
+			const options: ApiHandlerOptions = {
+				apiModelId: "llama2",
+				ollamaModelId: "llama2",
+				ollamaBaseUrl: "http://localhost:11434",
+				ollamaNumCtx: 4096, // Explicitly set num_ctx
+			}
+
+			handler = new NativeOllamaHandler(options)
+
+			mockChat.mockResolvedValue({
+				message: { content: "Response" },
+			})
+
+			await handler.completePrompt("Test prompt")
+
+			// Verify that num_ctx was included with the specified value
+			expect(mockChat).toHaveBeenCalledWith(
+				expect.objectContaining({
+					options: expect.objectContaining({
+						num_ctx: 4096,
+					}),
+				}),
+			)
+		})
 	})
 
 	describe("error handling", () => {

+ 20 - 25
src/api/providers/__tests__/roo.spec.ts

@@ -36,26 +36,12 @@ vitest.mock("openai", () => {
 						return {
 							[Symbol.asyncIterator]: async function* () {
 								yield {
-									choices: [
-										{
-											delta: { content: "Test response" },
-											index: 0,
-										},
-									],
+									choices: [{ delta: { content: "Test response" }, index: 0 }],
 									usage: null,
 								}
 								yield {
-									choices: [
-										{
-											delta: {},
-											index: 0,
-										},
-									],
-									usage: {
-										prompt_tokens: 10,
-										completion_tokens: 5,
-										total_tokens: 15,
-									},
+									choices: [{ delta: {}, index: 0 }],
+									usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
 								}
 							},
 						}
@@ -73,6 +59,7 @@ const mockHasInstance = vitest.fn()
 // Create mock functions that we can control
 const mockGetSessionTokenFn = vitest.fn()
 const mockHasInstanceFn = vitest.fn()
+const mockOnFn = vitest.fn()
 
 vitest.mock("@roo-code/cloud", () => ({
 	CloudService: {
@@ -82,6 +69,8 @@ vitest.mock("@roo-code/cloud", () => ({
 				authService: {
 					getSessionToken: () => mockGetSessionTokenFn(),
 				},
+				on: vitest.fn(),
+				off: vitest.fn(),
 			}
 		},
 	},
@@ -409,11 +398,18 @@ describe("RooHandler", () => {
 		it("should handle undefined auth service gracefully", () => {
 			mockHasInstanceFn.mockReturnValue(true)
 			// Mock CloudService with undefined authService
-			const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get
+			const originalGetSessionToken = mockGetSessionTokenFn.getMockImplementation()
+
+			// Temporarily make authService return undefined
+			mockGetSessionTokenFn.mockImplementation(() => undefined)
 
 			try {
 				Object.defineProperty(CloudService, "instance", {
-					get: () => ({ authService: undefined }),
+					get: () => ({
+						authService: undefined,
+						on: vitest.fn(),
+						off: vitest.fn(),
+					}),
 					configurable: true,
 				})
 
@@ -424,12 +420,11 @@ describe("RooHandler", () => {
 				const handler = new RooHandler(mockOptions)
 				expect(handler).toBeInstanceOf(RooHandler)
 			} finally {
-				// Always restore original getter, even if test fails
-				if (originalGetter) {
-					Object.defineProperty(CloudService, "instance", {
-						get: originalGetter,
-						configurable: true,
-					})
+				// Restore original mock implementation
+				if (originalGetSessionToken) {
+					mockGetSessionTokenFn.mockImplementation(originalGetSessionToken)
+				} else {
+					mockGetSessionTokenFn.mockReturnValue("test-session-token")
 				}
 			}
 		})

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

@@ -115,7 +115,7 @@ describe("ZAiHandler", () => {
 			const handlerDefault = new ZAiHandler({ zaiApiKey: "test-zai-api-key" })
 			expect(OpenAI).toHaveBeenCalledWith(
 				expect.objectContaining({
-					baseURL: "https://api.z.ai/api/coding/paas/v4", // kilocode_change, upstream pr pending
+					baseURL: "https://api.z.ai/api/coding/paas/v4",
 				}),
 			)
 

+ 22 - 58
src/api/providers/fetchers/huggingface.ts

@@ -1,17 +1,16 @@
 import axios from "axios"
 import { z } from "zod"
-import type { ModelInfo } from "@roo-code/types"
+
 import {
+	type ModelInfo,
 	HUGGINGFACE_API_URL,
 	HUGGINGFACE_CACHE_DURATION,
 	HUGGINGFACE_DEFAULT_MAX_TOKENS,
 	HUGGINGFACE_DEFAULT_CONTEXT_WINDOW,
 } from "@roo-code/types"
+
 import type { ModelRecord } from "../../../shared/api"
 
-/**
- * HuggingFace Provider Schema
- */
 const huggingFaceProviderSchema = z.object({
 	provider: z.string(),
 	status: z.enum(["live", "staging", "error"]),
@@ -27,7 +26,8 @@ const huggingFaceProviderSchema = z.object({
 })
 
 /**
- * Represents a provider that can serve a HuggingFace model
+ * Represents a provider that can serve a HuggingFace model.
+ *
  * @property provider - The provider identifier (e.g., "sambanova", "together")
  * @property status - The current status of the provider
  * @property supports_tools - Whether the provider supports tool/function calling
@@ -37,9 +37,6 @@ const huggingFaceProviderSchema = z.object({
  */
 export type HuggingFaceProvider = z.infer<typeof huggingFaceProviderSchema>
 
-/**
- * HuggingFace Model Schema
- */
 const huggingFaceModelSchema = z.object({
 	id: z.string(),
 	object: z.literal("model"),
@@ -50,6 +47,7 @@ const huggingFaceModelSchema = z.object({
 
 /**
  * Represents a HuggingFace model available through the router API
+ *
  * @property id - The unique identifier of the model
  * @property object - The object type (always "model")
  * @property created - Unix timestamp of when the model was created
@@ -58,26 +56,13 @@ const huggingFaceModelSchema = z.object({
  */
 export type HuggingFaceModel = z.infer<typeof huggingFaceModelSchema>
 
-/**
- * HuggingFace API Response Schema
- */
 const huggingFaceApiResponseSchema = z.object({
 	object: z.string(),
 	data: z.array(huggingFaceModelSchema),
 })
 
-/**
- * Represents the response from the HuggingFace router API
- * @property object - The response object type
- * @property data - Array of available models
- */
 type HuggingFaceApiResponse = z.infer<typeof huggingFaceApiResponseSchema>
 
-/**
- * Cache entry for storing fetched models
- * @property data - The cached model records
- * @property timestamp - Unix timestamp of when the cache was last updated
- */
 interface CacheEntry {
 	data: ModelRecord
 	rawModels?: HuggingFaceModel[]
@@ -87,13 +72,14 @@ interface CacheEntry {
 let cache: CacheEntry | null = null
 
 /**
- * Parse a HuggingFace model into ModelInfo format
+ * Parse a HuggingFace model into ModelInfo format.
+ *
  * @param model - The HuggingFace model to parse
  * @param provider - Optional specific provider to use for capabilities
  * @returns ModelInfo object compatible with the application's model system
  */
 function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFaceProvider): ModelInfo {
-	// Use provider-specific values if available, otherwise find first provider with values
+	// Use provider-specific values if available, otherwise find first provider with values.
 	const contextLength =
 		provider?.context_length ||
 		model.providers.find((p) => p.context_length)?.context_length ||
@@ -101,13 +87,13 @@ function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFacePr
 
 	const pricing = provider?.pricing || model.providers.find((p) => p.pricing)?.pricing
 
-	// Include provider name in description if specific provider is given
+	// Include provider name in description if specific provider is given.
 	const description = provider ? `${model.id} via ${provider.provider}` : `${model.id} via HuggingFace`
 
 	return {
 		maxTokens: Math.min(contextLength, HUGGINGFACE_DEFAULT_MAX_TOKENS),
 		contextWindow: contextLength,
-		supportsImages: false, // HuggingFace API doesn't provide this info yet
+		supportsImages: false, // HuggingFace API doesn't provide this info yet.
 		supportsPromptCache: false,
 		supportsComputerUse: false,
 		inputPrice: pricing?.input,
@@ -125,7 +111,6 @@ function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFacePr
 export async function getHuggingFaceModels(): Promise<ModelRecord> {
 	const now = Date.now()
 
-	// Check cache
 	if (cache && now - cache.timestamp < HUGGINGFACE_CACHE_DURATION) {
 		return cache.data
 	}
@@ -144,7 +129,7 @@ export async function getHuggingFaceModels(): Promise<ModelRecord> {
 				Pragma: "no-cache",
 				"Cache-Control": "no-cache",
 			},
-			timeout: 10000, // 10 second timeout
+			timeout: 10000,
 		})
 
 		const result = huggingFaceApiResponseSchema.safeParse(response.data)
@@ -157,38 +142,31 @@ export async function getHuggingFaceModels(): Promise<ModelRecord> {
 		const validModels = result.data.data.filter((model) => model.providers.length > 0)
 
 		for (const model of validModels) {
-			// Add the base model
+			// Add the base model.
 			models[model.id] = parseHuggingFaceModel(model)
 
-			// Add provider-specific variants for all live providers
+			// Add provider-specific variants for all live providers.
 			for (const provider of model.providers) {
 				if (provider.status === "live") {
 					const providerKey = `${model.id}:${provider.provider}`
 					const providerModel = parseHuggingFaceModel(model, provider)
 
-					// Always add provider variants to show all available providers
+					// Always add provider variants to show all available providers.
 					models[providerKey] = providerModel
 				}
 			}
 		}
 
-		// Update cache
-		cache = {
-			data: models,
-			rawModels: validModels,
-			timestamp: now,
-		}
+		cache = { data: models, rawModels: validModels, timestamp: now }
 
 		return models
 	} catch (error) {
 		console.error("Error fetching HuggingFace models:", error)
 
-		// Return cached data if available
 		if (cache) {
 			return cache.data
 		}
 
-		// Re-throw with more context
 		if (axios.isAxiosError(error)) {
 			if (error.response) {
 				throw new Error(
@@ -208,45 +186,35 @@ export async function getHuggingFaceModels(): Promise<ModelRecord> {
 }
 
 /**
- * Get cached models without making an API request
+ * Get cached models without making an API request.
  */
 export function getCachedHuggingFaceModels(): ModelRecord | null {
 	return cache?.data || null
 }
 
 /**
- * Get cached raw models for UI display
+ * Get cached raw models for UI display.
  */
 export function getCachedRawHuggingFaceModels(): HuggingFaceModel[] | null {
 	return cache?.rawModels || null
 }
 
-/**
- * Clear the cache
- */
 export function clearHuggingFaceCache(): void {
 	cache = null
 }
 
-/**
- * HuggingFace Models Response Interface
- */
 export interface HuggingFaceModelsResponse {
 	models: HuggingFaceModel[]
 	cached: boolean
 	timestamp: number
 }
 
-/**
- * Get HuggingFace models with response metadata
- * This function provides a higher-level API that includes cache status and timestamp
- */
 export async function getHuggingFaceModelsWithMetadata(): Promise<HuggingFaceModelsResponse> {
 	try {
-		// First, trigger the fetch to populate cache
+		// First, trigger the fetch to populate cache.
 		await getHuggingFaceModels()
 
-		// Get the raw models from cache
+		// Get the raw models from cache.
 		const cachedRawModels = getCachedRawHuggingFaceModels()
 
 		if (cachedRawModels) {
@@ -257,7 +225,7 @@ export async function getHuggingFaceModelsWithMetadata(): Promise<HuggingFaceMod
 			}
 		}
 
-		// If no cached raw models, fetch directly from API
+		// If no cached raw models, fetch directly from API.
 		const response = await axios.get(HUGGINGFACE_API_URL, {
 			headers: {
 				"Upgrade-Insecure-Requests": "1",
@@ -281,10 +249,6 @@ export async function getHuggingFaceModelsWithMetadata(): Promise<HuggingFaceMod
 		}
 	} catch (error) {
 		console.error("Failed to get HuggingFace models:", error)
-		return {
-			models: [],
-			cached: false,
-			timestamp: Date.now(),
-		}
+		return { models: [], cached: false, timestamp: Date.now() }
 	}
 }

+ 6 - 34
src/api/providers/fetchers/io-intelligence.ts

@@ -1,12 +1,10 @@
 import axios from "axios"
 import { z } from "zod"
-import type { ModelInfo } from "@roo-code/types"
-import { IO_INTELLIGENCE_CACHE_DURATION } from "@roo-code/types"
+
+import { type ModelInfo, IO_INTELLIGENCE_CACHE_DURATION } from "@roo-code/types"
+
 import type { ModelRecord } from "../../../shared/api"
 
-/**
- * IO Intelligence Model Schema
- */
 const ioIntelligenceModelSchema = z.object({
 	id: z.string(),
 	object: z.literal("model"),
@@ -35,9 +33,6 @@ const ioIntelligenceModelSchema = z.object({
 
 export type IOIntelligenceModel = z.infer<typeof ioIntelligenceModelSchema>
 
-/**
- * IO Intelligence API Response Schema
- */
 const ioIntelligenceApiResponseSchema = z.object({
 	object: z.literal("list"),
 	data: z.array(ioIntelligenceModelSchema),
@@ -45,9 +40,6 @@ const ioIntelligenceApiResponseSchema = z.object({
 
 type IOIntelligenceApiResponse = z.infer<typeof ioIntelligenceApiResponseSchema>
 
-/**
- * Cache entry for storing fetched models
- */
 interface CacheEntry {
 	data: ModelRecord
 	timestamp: number
@@ -66,21 +58,15 @@ const MODEL_CONTEXT_LENGTHS: Record<string, number> = {
 	"openai/gpt-oss-120b": 131072,
 }
 
-/**
- * Vision models that support images
- */
 const VISION_MODELS = new Set([
 	"Qwen/Qwen2.5-VL-32B-Instruct",
 	"meta-llama/Llama-3.2-90B-Vision-Instruct",
 	"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
 ])
 
-/**
- * Parse an IO Intelligence model into ModelInfo format
- */
 function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo {
 	const contextLength = MODEL_CONTEXT_LENGTHS[model.id] || 8192
-	// Cap maxTokens at 32k for very large context windows, or 20% of context length, whichever is smaller
+	// Cap maxTokens at 32k for very large context windows, or 20% of context length, whichever is smaller.
 	const maxTokens = Math.min(contextLength, Math.ceil(contextLength * 0.2), 32768)
 	const supportsImages = VISION_MODELS.has(model.id)
 
@@ -101,7 +87,6 @@ function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo {
 export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRecord> {
 	const now = Date.now()
 
-	// Check cache
 	if (cache && now - cache.timestamp < IO_INTELLIGENCE_CACHE_DURATION) {
 		return cache.data
 	}
@@ -113,7 +98,6 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRec
 			"Content-Type": "application/json",
 		}
 
-		// Add authorization header if API key is provided
 		if (apiKey) {
 			headers.Authorization = `Bearer ${apiKey}`
 		} else {
@@ -125,7 +109,7 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRec
 			"https://api.intelligence.io.solutions/api/v1/models",
 			{
 				headers,
-				timeout: 10000, // 10 second timeout
+				timeout: 10_000,
 			},
 		)
 
@@ -140,22 +124,16 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRec
 			models[model.id] = parseIOIntelligenceModel(model)
 		}
 
-		// Update cache
-		cache = {
-			data: models,
-			timestamp: now,
-		}
+		cache = { data: models, timestamp: now }
 
 		return models
 	} catch (error) {
 		console.error("Error fetching IO Intelligence models:", error)
 
-		// Return cached data if available
 		if (cache) {
 			return cache.data
 		}
 
-		// Re-throw with more context
 		if (axios.isAxiosError(error)) {
 			if (error.response) {
 				throw new Error(
@@ -174,16 +152,10 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRec
 	}
 }
 
-/**
- * Get cached models without making an API request
- */
 export function getCachedIOIntelligenceModels(): ModelRecord | null {
 	return cache?.data || null
 }
 
-/**
- * Clear the cache
- */
 export function clearIOIntelligenceCache(): void {
 	cache = null
 }

+ 9 - 9
src/api/providers/fetchers/lmstudio.ts

@@ -1,27 +1,27 @@
-import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types"
-import { LLM, LLMInfo, LLMInstanceInfo, LMStudioClient } from "@lmstudio/sdk"
 import axios from "axios"
+import { LLM, LLMInfo, LLMInstanceInfo, LMStudioClient } from "@lmstudio/sdk"
+
+import { type ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types"
+
 import { flushModels, getModels } from "./modelCache"
 
 const modelsWithLoadedDetails = new Set<string>()
 
-export const hasLoadedFullDetails = (modelId: string): boolean => {
-	return modelsWithLoadedDetails.has(modelId)
-}
+export const hasLoadedFullDetails = (modelId: string): boolean => modelsWithLoadedDetails.has(modelId)
 
 export const forceFullModelDetailsLoad = async (baseUrl: string, modelId: string): Promise<void> => {
 	try {
-		// test the connection to LM Studio first
-		// errors will be caught further down
+		// Test the connection to LM Studio first
+		// Crrors will be caught further down.
 		await axios.get(`${baseUrl}/v1/models`)
 		const lmsUrl = baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://")
 
 		const client = new LMStudioClient({ baseUrl: lmsUrl })
 		await client.llm.model(modelId)
 		await flushModels("lmstudio")
-		await getModels({ provider: "lmstudio" }) // force cache update now
+		await getModels({ provider: "lmstudio" }) // Force cache update now.
 
-		// Mark this model as having full details loaded
+		// Mark this model as having full details loaded.
 		modelsWithLoadedDetails.add(modelId)
 	} catch (error) {
 		if (error.code === "ECONNREFUSED") {

+ 20 - 17
src/api/providers/fetchers/modelCache.ts

@@ -2,11 +2,14 @@ import * as path from "path"
 import fs from "fs/promises"
 
 import NodeCache from "node-cache"
+
+import type { ProviderName } from "@roo-code/types"
+
 import { safeWriteJson } from "../../../utils/safeWriteJson"
 
 import { ContextProxy } from "../../../core/config/ContextProxy"
 import { getCacheDirectoryPath } from "../../../utils/storage"
-import { RouterName, ModelRecord } from "../../../shared/api"
+import type { RouterName, ModelRecord } from "../../../shared/api"
 import { fileExistsAtPath } from "../../../utils/fs"
 
 import { getOpenRouterModels } from "./openrouter"
@@ -21,11 +24,9 @@ import { getOllamaModels } from "./ollama"
 import { getLMStudioModels } from "./lmstudio"
 import { getIOIntelligenceModels } from "./io-intelligence"
 
-// kilocode_change start
-import { cerebrasModels } from "@roo-code/types"
-// kilocode_change end
-
 import { getDeepInfraModels } from "./deepinfra"
+import { getHuggingFaceModels } from "./huggingface"
+
 const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
 
 export /*kilocode_change*/ async function writeModels(router: RouterName, data: ModelRecord) {
@@ -55,7 +56,9 @@ export /*kilocode_change*/ async function readModels(router: RouterName): Promis
  */
 export const getModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
 	const { provider } = options
+
 	let models = getModelsFromCache(provider)
+
 	if (models) {
 		return models
 	}
@@ -71,18 +74,18 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 				// kilocode_change end
 				break
 			case "requesty":
-				// Requesty models endpoint requires an API key for per-user custom policies
+				// Requesty models endpoint requires an API key for per-user custom policies.
 				models = await getRequestyModels(options.baseUrl, options.apiKey)
 				break
 			case "glama":
 				models = await getGlamaModels()
 				break
 			case "unbound":
-				// Unbound models endpoint requires an API key to fetch application specific models
+				// Unbound models endpoint requires an API key to fetch application specific models.
 				models = await getUnboundModels(options.apiKey)
 				break
 			case "litellm":
-				// Type safety ensures apiKey and baseUrl are always provided for litellm
+				// Type safety ensures apiKey and baseUrl are always provided for LiteLLM.
 				models = await getLiteLLMModels(options.apiKey, options.baseUrl)
 				break
 			// kilocode_change start
@@ -97,11 +100,8 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 				})
 				break
 			// kilocode_change end
-			case "cerebras":
-				models = cerebrasModels
-				break
 			case "ollama":
-				models = await getOllamaModels(options.baseUrl, options.apiKey)
+				models = await getOllamaModels(options.baseUrl, options.apiKey, options.numCtx /*kilocode_change*/)
 				break
 			case "lmstudio":
 				models = await getLMStudioModels(options.baseUrl)
@@ -115,14 +115,17 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 			case "vercel-ai-gateway":
 				models = await getVercelAiGatewayModels()
 				break
+			case "huggingface":
+				models = await getHuggingFaceModels()
+				break
 			default: {
-				// Ensures router is exhaustively checked if RouterName is a strict union
+				// Ensures router is exhaustively checked if RouterName is a strict union.
 				const exhaustiveCheck: never = provider
 				throw new Error(`Unknown provider: ${exhaustiveCheck}`)
 			}
 		}
 
-		// Cache the fetched models (even if empty, to signify a successful fetch with no models)
+		// Cache the fetched models (even if empty, to signify a successful fetch with no models).
 		memoryCache.set(provider, models)
 
 		/* kilocode_change: skip useless file IO
@@ -132,7 +135,6 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 
 		try {
 			models = await readModels(provider)
-			// console.log(`[getModels] read ${router} models from file cache`)
 		} catch (error) {
 			console.error(`[getModels] error reading ${provider} models from file cache`, error)
 		}
@@ -147,13 +149,14 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 }
 
 /**
- * Flush models memory cache for a specific router
+ * Flush models memory cache for a specific router.
+ *
  * @param router - The router to flush models for.
  */
 export const flushModels = async (router: RouterName) => {
 	memoryCache.del(router)
 }
 
-export function getModelsFromCache(provider: string) {
+export function getModelsFromCache(provider: ProviderName) {
 	return memoryCache.get<ModelRecord>(provider)
 }

+ 10 - 2
src/api/providers/fetchers/ollama.ts

@@ -39,7 +39,10 @@ type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema>
 
 export const parseOllamaModel = (
 	rawModel: OllamaModelInfoResponse,
-	baseUrl?: string, // kilocode_change
+	// kilocode_change start
+	baseUrl?: string,
+	numCtx?: number,
+	// kilocode_change end
 ): ModelInfo => {
 	// kilocode_change start
 	const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
@@ -54,6 +57,7 @@ export const parseOllamaModel = (
 	const contextLengthFromEnvironment = parseInt(process.env.OLLAMA_CONTEXT_LENGTH ?? "", 10) || undefined
 
 	const contextWindow =
+		numCtx ??
 		(baseUrl?.toLowerCase().startsWith("https://ollama.com") ? contextLengthFromModelInfo : undefined) ??
 		contextLengthFromEnvironment ??
 		(contextLengthFromModelParameters !== 40960 ? contextLengthFromModelParameters : undefined) ?? // Alledgedly Ollama sometimes returns an undefind context as 40960
@@ -75,6 +79,7 @@ export const parseOllamaModel = (
 export async function getOllamaModels(
 	baseUrl = "http://localhost:11434",
 	apiKey?: string,
+	numCtx?: number, // kilocode_change
 ): Promise<Record<string, ModelInfo>> {
 	const models: Record<string, ModelInfo> = {}
 
@@ -110,7 +115,10 @@ export async function getOllamaModels(
 						.then((ollamaModelInfo) => {
 							models[ollamaModel.name] = parseOllamaModel(
 								ollamaModelInfo.data,
-								baseUrl, // kilocode_change
+								// kilocode_change start
+								baseUrl,
+								numCtx,
+								// kilocode_change end
 							)
 						}),
 				)

+ 11 - 4
src/api/providers/gemini.ts

@@ -294,10 +294,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
 		outputTokens: number
 		cacheReadTokens?: number
 	}) {
-		if (!info.inputPrice || !info.outputPrice || !info.cacheReadsPrice) {
-			return undefined
-		}
-
+		// For models with tiered pricing, prices might only be defined in tiers
 		let inputPrice = info.inputPrice
 		let outputPrice = info.outputPrice
 		let cacheReadsPrice = info.cacheReadsPrice
@@ -314,6 +311,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
 			}
 		}
 
+		// Check if we have the required prices after considering tiers
+		if (!inputPrice || !outputPrice) {
+			return undefined
+		}
+
+		// cacheReadsPrice is optional - if not defined, treat as 0
+		if (!cacheReadsPrice) {
+			cacheReadsPrice = 0
+		}
+
 		// Subtract the cached input tokens from the total input tokens.
 		const uncachedInputTokens = inputTokens - cacheReadTokens
 

+ 3 - 1
src/api/providers/io-intelligence.ts

@@ -19,8 +19,10 @@ export class IOIntelligenceHandler extends BaseOpenAiCompatibleProvider<IOIntell
 			apiKey: options.ioIntelligenceApiKey,
 		})
 	}
+
 	override getModel() {
 		const modelId = this.options.ioIntelligenceModelId || (ioIntelligenceDefaultModelId as IOIntelligenceModelId)
+
 		const modelInfo =
 			this.providerModels[modelId as IOIntelligenceModelId] ?? this.providerModels[ioIntelligenceDefaultModelId]
 
@@ -28,7 +30,7 @@ export class IOIntelligenceHandler extends BaseOpenAiCompatibleProvider<IOIntell
 			return { id: modelId as IOIntelligenceModelId, info: modelInfo }
 		}
 
-		// Return the requested model ID even if not found, with fallback info
+		// Return the requested model ID even if not found, with fallback info.
 		return {
 			id: modelId as IOIntelligenceModelId,
 			info: {

+ 40 - 11
src/api/providers/native-ollama.ts

@@ -20,6 +20,11 @@ function estimateOllamaTokenCount(messages: Message[]): number {
 }
 // kilocode_change end
 
+interface OllamaChatOptions {
+	temperature: number
+	num_ctx?: number
+}
+
 function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
 	const ollamaMessages: Message[] = []
 
@@ -166,7 +171,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 			try {
 				// kilocode_change start
 				const headers = this.options.ollamaApiKey
-					? { Authorization: this.options.ollamaApiKey } // Yes, this is weird, its not a Bearer token
+					? { Authorization: `Bearer ${this.options.ollamaApiKey}` }
 					: undefined
 				// kilocode_change end
 
@@ -205,10 +210,12 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 		]
 
 		// kilocode_change start
+		// it is tedious we have to check this, but Ollama's quiet prompt-truncating behavior is a support nightmare otherwise
 		const estimatedTokenCount = estimateOllamaTokenCount(ollamaMessages)
-		if (modelInfo.maxTokens && estimatedTokenCount > modelInfo.maxTokens) {
+		const maxTokens = this.options.ollamaNumCtx ?? modelInfo.contextWindow
+		if (estimatedTokenCount > maxTokens) {
 			throw new Error(
-				`Input message is too long for the selected model. Estimated tokens: ${estimatedTokenCount}, Max tokens: ${modelInfo.maxTokens}. To increase the context window size, see: https://kilocode.ai/docs/providers/ollama#configure-the-context-size`,
+				`Prompt is too long (estimated tokens: ${estimatedTokenCount}, max tokens: ${maxTokens}). Increase the Context Window Size in Settings.`,
 			)
 		}
 		// kilocode_change end
@@ -223,14 +230,22 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 		)
 
 		try {
+			// Build options object conditionally
+			const chatOptions: OllamaChatOptions = {
+				temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
+			}
+
+			// Only include num_ctx if explicitly set via ollamaNumCtx
+			if (this.options.ollamaNumCtx !== undefined) {
+				chatOptions.num_ctx = this.options.ollamaNumCtx
+			}
+
 			// Create the actual API request promise
 			const stream = await client.chat({
 				model: modelId,
 				messages: ollamaMessages,
 				stream: true,
-				options: {
-					temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
-				},
+				options: chatOptions,
 			})
 
 			let totalInputTokens = 0
@@ -294,8 +309,14 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 	}
 
 	async fetchModel() {
-		this.models = await getOllamaModels(this.options.ollamaBaseUrl, this.options.ollamaApiKey)
-		return this.models // kilocode_change
+		// kilocode_change start
+		this.models = await getOllamaModels(
+			this.options.ollamaBaseUrl,
+			this.options.ollamaApiKey,
+			this.options.ollamaNumCtx,
+		)
+		return this.models
+		// kilocode_change end
 	}
 
 	override getModel(): { id: string; info: ModelInfo } {
@@ -331,13 +352,21 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
 			const { id: modelId } = this.getModel() // kilocode_change: fetchModel => getModel
 			const useR1Format = modelId.toLowerCase().includes("deepseek-r1")
 
+			// Build options object conditionally
+			const chatOptions: OllamaChatOptions = {
+				temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
+			}
+
+			// Only include num_ctx if explicitly set via ollamaNumCtx
+			if (this.options.ollamaNumCtx !== undefined) {
+				chatOptions.num_ctx = this.options.ollamaNumCtx
+			}
+
 			const response = await client.chat({
 				model: modelId,
 				messages: [{ role: "user", content: prompt }],
 				stream: false,
-				options: {
-					temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
-				},
+				options: chatOptions,
 			})
 
 			return response.message?.content || ""

+ 36 - 6
src/api/providers/roo.ts

@@ -1,22 +1,24 @@
 import { Anthropic } from "@anthropic-ai/sdk"
+import OpenAI from "openai"
 
-import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
+import { AuthState, rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
 import { CloudService } from "@roo-code/cloud"
 
 import type { ApiHandlerOptions } from "../../shared/api"
 import { ApiStream } from "../transform/stream"
 
 import type { ApiHandlerCreateMessageMetadata } from "../index"
+import { DEFAULT_HEADERS } from "./constants"
 import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
 
 export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
+	private authStateListener?: (state: { state: AuthState }) => void
+
 	constructor(options: ApiHandlerOptions) {
-		// Get the session token if available, but don't throw if not.
-		// The server will handle authentication errors and return appropriate status codes.
-		let sessionToken = ""
+		let sessionToken: string | undefined = undefined
 
 		if (CloudService.hasInstance()) {
-			sessionToken = CloudService.instance.authService?.getSessionToken() || ""
+			sessionToken = CloudService.instance.authService?.getSessionToken()
 		}
 
 		// Always construct the handler, even without a valid token.
@@ -25,11 +27,39 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
 			...options,
 			providerName: "Roo Code Cloud",
 			baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1",
-			apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token
+			apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token.
 			defaultProviderModelId: rooDefaultModelId,
 			providerModels: rooModels,
 			defaultTemperature: 0.7,
 		})
+
+		if (CloudService.hasInstance()) {
+			const cloudService = CloudService.instance
+
+			this.authStateListener = (state: { state: AuthState }) => {
+				if (state.state === "active-session") {
+					this.client = new OpenAI({
+						baseURL: this.baseURL,
+						apiKey: cloudService.authService?.getSessionToken() ?? "unauthenticated",
+						defaultHeaders: DEFAULT_HEADERS,
+					})
+				} else if (state.state === "logged-out") {
+					this.client = new OpenAI({
+						baseURL: this.baseURL,
+						apiKey: "unauthenticated",
+						defaultHeaders: DEFAULT_HEADERS,
+					})
+				}
+			}
+
+			cloudService.on("auth-state-changed", this.authStateListener)
+		}
+	}
+
+	dispose() {
+		if (this.authStateListener && CloudService.hasInstance()) {
+			CloudService.instance.off("auth-state-changed", this.authStateListener)
+		}
 	}
 
 	override async *createMessage(

+ 3 - 3
src/api/providers/zai.ts

@@ -6,7 +6,7 @@ import {
 	type InternationalZAiModelId,
 	type MainlandZAiModelId,
 	ZAI_DEFAULT_TEMPERATURE,
-	zaiApiLineConfigs, // kilocode_change
+	zaiApiLineConfigs,
 } from "@roo-code/types"
 
 import type { ApiHandlerOptions } from "../../shared/api"
@@ -15,14 +15,14 @@ import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
 
 export class ZAiHandler extends BaseOpenAiCompatibleProvider<InternationalZAiModelId | MainlandZAiModelId> {
 	constructor(options: ApiHandlerOptions) {
-		const isChina = zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].isChina // kilocode_change
+		const isChina = zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].isChina
 		const models = isChina ? mainlandZAiModels : internationalZAiModels
 		const defaultModelId = isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
 
 		super({
 			...options,
 			providerName: "Z AI",
-			baseURL: zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].baseUrl, // kilocode_change
+			baseURL: zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].baseUrl,
 			apiKey: options.zaiApiKey ?? "not-provided",
 			defaultProviderModelId: defaultModelId,
 			providerModels: models,

+ 35 - 31
src/core/prompts/__tests__/custom-system-prompt.spec.ts

@@ -83,38 +83,42 @@ describe("File-Based Custom System Prompt", () => {
 		mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
 	})
 
-	it("should use default generation when no file-based system prompt is found", async () => {
-		const customModePrompts = {
-			[defaultModeSlug]: {
-				roleDefinition: "Test role definition",
-			},
-		}
-
-		const prompt = await SYSTEM_PROMPT(
-			mockContext,
-			"test/path", // Using a relative path without leading slash
-			false, // supportsComputerUse
-			undefined, // mcpHub
-			undefined, // diffStrategy
-			undefined, // browserViewportSize
-			defaultModeSlug, // mode
-			customModePrompts, // customModePrompts
-			undefined, // customModes
-			undefined, // globalCustomInstructions
-			undefined, // diffEnabled
-			undefined, // experiments
-			true, // enableMcpServerCreation
-			undefined, // language
-			undefined, // rooIgnoreInstructions
-			undefined, // partialReadsEnabled
-		)
+	// Skipped on Windows due to timeout/flake issues
+	it.skipIf(process.platform === "win32")(
+		"should use default generation when no file-based system prompt is found",
+		async () => {
+			const customModePrompts = {
+				[defaultModeSlug]: {
+					roleDefinition: "Test role definition",
+				},
+			}
 
-		// Should contain default sections
-		expect(prompt).toContain("TOOL USE")
-		expect(prompt).toContain("CAPABILITIES")
-		expect(prompt).toContain("MODES")
-		expect(prompt).toContain("Test role definition")
-	})
+			const prompt = await SYSTEM_PROMPT(
+				mockContext,
+				"test/path", // Using a relative path without leading slash
+				false, // supportsComputerUse
+				undefined, // mcpHub
+				undefined, // diffStrategy
+				undefined, // browserViewportSize
+				defaultModeSlug, // mode
+				customModePrompts, // customModePrompts
+				undefined, // customModes
+				undefined, // globalCustomInstructions
+				undefined, // diffEnabled
+				undefined, // experiments
+				true, // enableMcpServerCreation
+				undefined, // language
+				undefined, // rooIgnoreInstructions
+				undefined, // partialReadsEnabled
+			)
+
+			// Should contain default sections
+			expect(prompt).toContain("TOOL USE")
+			expect(prompt).toContain("CAPABILITIES")
+			expect(prompt).toContain("MODES")
+			expect(prompt).toContain("Test role definition")
+		},
+	)
 
 	it("should use file-based custom system prompt when available", async () => {
 		// Mock the readFile to return content from a file

+ 35 - 31
src/core/task/Task.ts

@@ -224,6 +224,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 
 	didFinishAbortingStream = false
 	abandoned = false
+	abortReason?: ClineApiReqCancelReason
 	isInitialized = false
 	isPaused: boolean = false
 	pausedModeSlug: string = defaultModeSlug
@@ -1298,6 +1299,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			modifiedClineMessages.splice(lastRelevantMessageIndex + 1)
 		}
 
+		// Remove any trailing reasoning-only UI messages that were not part of the persisted API conversation
+		while (modifiedClineMessages.length > 0) {
+			const last = modifiedClineMessages[modifiedClineMessages.length - 1]
+			if (last.type === "say" && last.say === "reasoning") {
+				modifiedClineMessages.pop()
+			} else {
+				break
+			}
+		}
+
 		// Since we don't use `api_req_finished` anymore, we need to check if the
 		// last `api_req_started` has a cost value, if it doesn't and no
 		// cancellation reason to present, then we remove it since it indicates
@@ -1936,28 +1947,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 						lastMessage.partial = false
 						// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
 						console.log("updating partial message", lastMessage)
-						// await this.saveClineMessages()
 					}
 
-					// Let assistant know their response was interrupted for when task is resumed
-					await this.addToApiConversationHistory({
-						role: "assistant",
-						content: [
-							{
-								type: "text",
-								text:
-									assistantMessage +
-									`\n\n[${
-										cancelReason === "streaming_failed"
-											? "Response interrupted by API Error"
-											: "Response interrupted by user"
-									}]`,
-							},
-						],
-					})
-
 					// Update `api_req_started` to have cancelled and cost, so that
-					// we can display the cost of the partial stream.
+					// we can display the cost of the partial stream and the cancellation reason
 					updateApiReqMsg(cancelReason, streamingFailedMessage)
 					await this.saveClineMessages()
 
@@ -2003,10 +1996,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 						}
 
 						switch (chunk.type) {
-							case "reasoning":
+							case "reasoning": {
 								reasoningMessage += chunk.text
-								await this.say("reasoning", reasoningMessage, undefined, true)
+								// Only apply formatting if the message contains sentence-ending punctuation followed by **
+								let formattedReasoning = reasoningMessage
+								if (reasoningMessage.includes("**")) {
+									// Add line breaks before **Title** patterns that appear after sentence endings
+									// This targets section headers like "...end of sentence.**Title Here**"
+									// Handles periods, exclamation marks, and question marks
+									formattedReasoning = reasoningMessage.replace(
+										/([.!?])\*\*([^*\n]+)\*\*/g,
+										"$1\n\n**$2**",
+									)
+								}
+								await this.say("reasoning", formattedReasoning, undefined, true)
 								break
+							}
 							case "usage":
 								inputTokens += chunk.inputTokens
 								outputTokens += chunk.outputTokens
@@ -2263,24 +2268,23 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 						// may have executed), so we just resort to replicating a
 						// cancel task.
 
-						// Check if this was a user-initiated cancellation BEFORE calling abortTask
-						// If this.abort is already true, it means the user clicked cancel, so we should
-						// treat this as "user_cancelled" rather than "streaming_failed"
-						const cancelReason = this.abort ? "user_cancelled" : "streaming_failed"
+						// Determine cancellation reason BEFORE aborting to ensure correct persistence
+						const cancelReason: ClineApiReqCancelReason = this.abort ? "user_cancelled" : "streaming_failed"
 
 						const streamingFailedMessage = this.abort
 							? undefined
 							: (error.message ?? JSON.stringify(serializeError(error), null, 2))
 
-						// Now call abortTask after determining the cancel reason.
-						await this.abortTask()
+						// Persist interruption details first to both UI and API histories
 						await abortStream(cancelReason, streamingFailedMessage)
 
-						const history = await provider?.getTaskWithId(this.taskId)
+						// Record reason for provider to decide rehydration path
+						this.abortReason = cancelReason
 
-						if (history) {
-							await provider?.createTaskWithHistoryItem(history.historyItem)
-						}
+						// Now abort (emits TaskAborted which provider listens to)
+						await this.abortTask()
+
+						// Do not rehydrate here; provider owns rehydration to avoid duplication races
 					}
 				} finally {
 					this.isStreaming = false

+ 243 - 0
src/core/tools/__tests__/updateTodoListTool.spec.ts

@@ -0,0 +1,243 @@
+import { describe, it, expect, beforeEach, vi } from "vitest"
+import { parseMarkdownChecklist } from "../updateTodoListTool"
+import { TodoItem } from "@roo-code/types"
+
+describe("parseMarkdownChecklist", () => {
+	describe("standard checkbox format (without dash prefix)", () => {
+		it("should parse pending tasks", () => {
+			const md = `[ ] Task 1
+[ ] Task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Task 1")
+			expect(result[0].status).toBe("pending")
+			expect(result[1].content).toBe("Task 2")
+			expect(result[1].status).toBe("pending")
+		})
+
+		it("should parse completed tasks with lowercase x", () => {
+			const md = `[x] Completed task 1
+[x] Completed task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Completed task 1")
+			expect(result[0].status).toBe("completed")
+			expect(result[1].content).toBe("Completed task 2")
+			expect(result[1].status).toBe("completed")
+		})
+
+		it("should parse completed tasks with uppercase X", () => {
+			const md = `[X] Completed task 1
+[X] Completed task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Completed task 1")
+			expect(result[0].status).toBe("completed")
+			expect(result[1].content).toBe("Completed task 2")
+			expect(result[1].status).toBe("completed")
+		})
+
+		it("should parse in-progress tasks with dash", () => {
+			const md = `[-] In progress task 1
+[-] In progress task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("In progress task 1")
+			expect(result[0].status).toBe("in_progress")
+			expect(result[1].content).toBe("In progress task 2")
+			expect(result[1].status).toBe("in_progress")
+		})
+
+		it("should parse in-progress tasks with tilde", () => {
+			const md = `[~] In progress task 1
+[~] In progress task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("In progress task 1")
+			expect(result[0].status).toBe("in_progress")
+			expect(result[1].content).toBe("In progress task 2")
+			expect(result[1].status).toBe("in_progress")
+		})
+	})
+
+	describe("dash-prefixed checkbox format", () => {
+		it("should parse pending tasks with dash prefix", () => {
+			const md = `- [ ] Task 1
+- [ ] Task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Task 1")
+			expect(result[0].status).toBe("pending")
+			expect(result[1].content).toBe("Task 2")
+			expect(result[1].status).toBe("pending")
+		})
+
+		it("should parse completed tasks with dash prefix and lowercase x", () => {
+			const md = `- [x] Completed task 1
+- [x] Completed task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Completed task 1")
+			expect(result[0].status).toBe("completed")
+			expect(result[1].content).toBe("Completed task 2")
+			expect(result[1].status).toBe("completed")
+		})
+
+		it("should parse completed tasks with dash prefix and uppercase X", () => {
+			const md = `- [X] Completed task 1
+- [X] Completed task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Completed task 1")
+			expect(result[0].status).toBe("completed")
+			expect(result[1].content).toBe("Completed task 2")
+			expect(result[1].status).toBe("completed")
+		})
+
+		it("should parse in-progress tasks with dash prefix and dash marker", () => {
+			const md = `- [-] In progress task 1
+- [-] In progress task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("In progress task 1")
+			expect(result[0].status).toBe("in_progress")
+			expect(result[1].content).toBe("In progress task 2")
+			expect(result[1].status).toBe("in_progress")
+		})
+
+		it("should parse in-progress tasks with dash prefix and tilde marker", () => {
+			const md = `- [~] In progress task 1
+- [~] In progress task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("In progress task 1")
+			expect(result[0].status).toBe("in_progress")
+			expect(result[1].content).toBe("In progress task 2")
+			expect(result[1].status).toBe("in_progress")
+		})
+	})
+
+	describe("mixed formats", () => {
+		it("should parse mixed formats correctly", () => {
+			const md = `[ ] Task without dash
+- [ ] Task with dash
+[x] Completed without dash
+- [X] Completed with dash
+[-] In progress without dash
+- [~] In progress with dash`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(6)
+
+			expect(result[0].content).toBe("Task without dash")
+			expect(result[0].status).toBe("pending")
+
+			expect(result[1].content).toBe("Task with dash")
+			expect(result[1].status).toBe("pending")
+
+			expect(result[2].content).toBe("Completed without dash")
+			expect(result[2].status).toBe("completed")
+
+			expect(result[3].content).toBe("Completed with dash")
+			expect(result[3].status).toBe("completed")
+
+			expect(result[4].content).toBe("In progress without dash")
+			expect(result[4].status).toBe("in_progress")
+
+			expect(result[5].content).toBe("In progress with dash")
+			expect(result[5].status).toBe("in_progress")
+		})
+	})
+
+	describe("edge cases", () => {
+		it("should handle empty strings", () => {
+			const result = parseMarkdownChecklist("")
+			expect(result).toEqual([])
+		})
+
+		it("should handle non-string input", () => {
+			const result = parseMarkdownChecklist(null as any)
+			expect(result).toEqual([])
+		})
+
+		it("should handle undefined input", () => {
+			const result = parseMarkdownChecklist(undefined as any)
+			expect(result).toEqual([])
+		})
+
+		it("should ignore non-checklist lines", () => {
+			const md = `This is not a checklist
+[ ] Valid task
+Just some text
+- Not a checklist item
+- [x] Valid completed task
+[not valid] Invalid format`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(2)
+			expect(result[0].content).toBe("Valid task")
+			expect(result[0].status).toBe("pending")
+			expect(result[1].content).toBe("Valid completed task")
+			expect(result[1].status).toBe("completed")
+		})
+
+		it("should handle extra spaces", () => {
+			const md = `  [ ]   Task with spaces  
+-  [ ]  Task with dash and spaces
+  [x]  Completed with spaces
+-   [X]   Completed with dash and spaces`
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(4)
+			expect(result[0].content).toBe("Task with spaces")
+			expect(result[1].content).toBe("Task with dash and spaces")
+			expect(result[2].content).toBe("Completed with spaces")
+			expect(result[3].content).toBe("Completed with dash and spaces")
+		})
+
+		it("should handle Windows line endings", () => {
+			const md = "[ ] Task 1\r\n- [x] Task 2\r\n[-] Task 3"
+			const result = parseMarkdownChecklist(md)
+			expect(result).toHaveLength(3)
+			expect(result[0].content).toBe("Task 1")
+			expect(result[0].status).toBe("pending")
+			expect(result[1].content).toBe("Task 2")
+			expect(result[1].status).toBe("completed")
+			expect(result[2].content).toBe("Task 3")
+			expect(result[2].status).toBe("in_progress")
+		})
+	})
+
+	describe("ID generation", () => {
+		it("should generate consistent IDs for the same content and status", () => {
+			const md1 = `[ ] Task 1
+[x] Task 2`
+			const md2 = `[ ] Task 1
+[x] Task 2`
+			const result1 = parseMarkdownChecklist(md1)
+			const result2 = parseMarkdownChecklist(md2)
+
+			expect(result1[0].id).toBe(result2[0].id)
+			expect(result1[1].id).toBe(result2[1].id)
+		})
+
+		it("should generate different IDs for different content", () => {
+			const md = `[ ] Task 1
+[ ] Task 2`
+			const result = parseMarkdownChecklist(md)
+			expect(result[0].id).not.toBe(result[1].id)
+		})
+
+		it("should generate different IDs for same content but different status", () => {
+			const md = `[ ] Task 1
+[x] Task 1`
+			const result = parseMarkdownChecklist(md)
+			expect(result[0].id).not.toBe(result[1].id)
+		})
+
+		it("should generate same IDs regardless of dash prefix", () => {
+			const md1 = `[ ] Task 1`
+			const md2 = `- [ ] Task 1`
+			const result1 = parseMarkdownChecklist(md1)
+			const result2 = parseMarkdownChecklist(md2)
+			expect(result1[0].id).toBe(result2[0].id)
+		})
+	})
+})

+ 2 - 1
src/core/tools/updateTodoListTool.ts

@@ -108,7 +108,8 @@ export function parseMarkdownChecklist(md: string): TodoItem[] {
 		.filter(Boolean)
 	const todos: TodoItem[] = []
 	for (const line of lines) {
-		const match = line.match(/^\[\s*([ xX\-~])\s*\]\s+(.+)$/)
+		// Support both "[ ] Task" and "- [ ] Task" formats
+		const match = line.match(/^(?:-\s*)?\[\s*([ xX\-~])\s*\]\s+(.+)$/)
 		if (!match) continue
 		let status: TodoStatus = "pending"
 		if (match[1] === "x" || match[1] === "X") status = "completed"

+ 76 - 8
src/core/webview/ClineProvider.ts

@@ -30,6 +30,7 @@ import {
 	type TerminalActionPromptType,
 	type HistoryItem,
 	type CloudUserInfo,
+	type CloudOrganizationMembership,
 	type CreateTaskOptions,
 	type TokenUsage,
 	RooCodeEventName,
@@ -90,6 +91,8 @@ import { Task } from "../task/Task"
 import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
 
 import { webviewMessageHandler } from "./webviewMessageHandler"
+import type { ClineMessage } from "@roo-code/types"
+import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
 import { getNonce } from "./getNonce"
 import { getUri } from "./getUri"
 
@@ -154,7 +157,7 @@ export class ClineProvider
 
 	public isViewLaunched = false
 	public settingsImportedAt?: number
-	public readonly latestAnnouncementId = "sep-2025-roo-code-cloud" // Roo Code Cloud announcement
+	public readonly latestAnnouncementId = "sep-2025-code-supernova" // Code Supernova stealth model announcement
 	public readonly providerSettingsManager: ProviderSettingsManager
 	public readonly customModesManager: CustomModesManager
 
@@ -209,7 +212,35 @@ export class ClineProvider
 			const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
 			const onTaskCompleted = (taskId: string, tokenUsage: any, toolUsage: any) =>
 				this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
-			const onTaskAborted = () => this.emit(RooCodeEventName.TaskAborted, instance.taskId)
+			const onTaskAborted = async () => {
+				this.emit(RooCodeEventName.TaskAborted, instance.taskId)
+
+				try {
+					// Only rehydrate on genuine streaming failures.
+					// User-initiated cancels are handled by cancelTask().
+					if (instance.abortReason === "streaming_failed") {
+						// Defensive safeguard: if another path already replaced this instance, skip
+						const current = this.getCurrentTask()
+						if (current && current.instanceId !== instance.instanceId) {
+							this.log(
+								`[onTaskAborted] Skipping rehydrate: current instance ${current.instanceId} != aborted ${instance.instanceId}`,
+							)
+							return
+						}
+
+						const { historyItem } = await this.getTaskWithId(instance.taskId)
+						const rootTask = instance.rootTask
+						const parentTask = instance.parentTask
+						await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
+					}
+				} catch (error) {
+					this.log(
+						`[onTaskAborted] Failed to rehydrate after streaming failure: ${
+							error instanceof Error ? error.message : String(error)
+						}`,
+					)
+				}
+			}
 			const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
 			const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
 			const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
@@ -1840,6 +1871,7 @@ export class ClineProvider
 			maxTotalImageSize,
 			terminalCompressProgressBar,
 			historyPreviewCollapsed,
+			reasoningBlockCollapsed,
 			cloudUserInfo,
 			cloudIsAuthenticated,
 			sharingEnabled,
@@ -1870,6 +1902,16 @@ export class ClineProvider
 			featureRoomoteControlEnabled,
 		} = await this.getState()
 
+		let cloudOrganizations: CloudOrganizationMembership[] = []
+
+		try {
+			cloudOrganizations = await CloudService.instance.getOrganizationMemberships()
+		} catch (error) {
+			console.error(
+				`[getStateToPostToWebview] failed to get cloud organizations: ${error instanceof Error ? error.message : String(error)}`,
+			)
+		}
+
 		const telemetryKey = process.env.KILOCODE_POSTHOG_API_KEY
 		const machineId = vscode.env.machineId
 
@@ -1985,8 +2027,10 @@ export class ClineProvider
 			terminalCompressProgressBar: terminalCompressProgressBar ?? true,
 			hasSystemPromptOverride,
 			historyPreviewCollapsed: historyPreviewCollapsed ?? false,
+			reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
 			cloudUserInfo,
 			cloudIsAuthenticated: cloudIsAuthenticated ?? false,
+			cloudOrganizations,
 			sharingEnabled: sharingEnabled ?? false,
 			organizationAllowList,
 			// kilocode_change start
@@ -2230,6 +2274,7 @@ export class ClineProvider
 			dismissedNotificationIds: stateValues.dismissedNotificationIds ?? [], // kilocode_change
 			morphApiKey: stateValues.morphApiKey, // kilocode_change
 			historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
+			reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
 			cloudUserInfo,
 			cloudIsAuthenticated,
 			sharingEnabled,
@@ -2671,14 +2716,24 @@ export class ClineProvider
 
 		console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
 
-		const { historyItem } = await this.getTaskWithId(task.taskId)
+		const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId)
 
 		// Preserve parent and root task information for history item.
 		const rootTask = task.rootTask
 		const parentTask = task.parentTask
 
+		// Mark this as a user-initiated cancellation so provider-only rehydration can occur
+		task.abortReason = "user_cancelled"
+
+		// Capture the current instance to detect if rehydrate already occurred elsewhere
+		const originalInstanceId = task.instanceId
+
+		// Begin abort (non-blocking)
 		task.abortTask()
 
+		// Immediately mark the original instance as abandoned to prevent any residual activity
+		task.abandoned = true
+
 		await pWaitFor(
 			() =>
 				this.getCurrentTask()! === undefined ||
@@ -2695,11 +2750,24 @@ export class ClineProvider
 			console.error("Failed to abort task")
 		})
 
-		if (this.getCurrentTask()) {
-			// 'abandoned' will prevent this Cline instance from affecting
-			// future Cline instances. This may happen if its hanging on a
-			// streaming request.
-			this.getCurrentTask()!.abandoned = true
+		// Defensive safeguard: if current instance already changed, skip rehydrate
+		const current = this.getCurrentTask()
+		if (current && current.instanceId !== originalInstanceId) {
+			this.log(
+				`[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
+			)
+			return
+		}
+
+		// Final race check before rehydrate to avoid duplicate rehydration
+		{
+			const currentAfterCheck = this.getCurrentTask()
+			if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
+				this.log(
+					`[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
+				)
+				return
+			}
 		}
 
 		// Clears task again, so we need to abortTask manually above.

+ 6 - 0
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -2752,6 +2752,8 @@ describe("ClineProvider - Router Models", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 	})
@@ -2804,6 +2806,8 @@ describe("ClineProvider - Router Models", () => {
 				litellm: {},
 				"kilocode-openrouter": {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 
@@ -2924,6 +2928,8 @@ describe("ClineProvider - Router Models", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 	})

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

@@ -1,3 +1,5 @@
+// npx vitest core/webview/__tests__/webviewMessageHandler.spec.ts
+
 import type { Mock } from "vitest"
 
 // Mock dependencies - must come before imports
@@ -228,6 +230,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			apiKey: "litellm-key",
 			baseUrl: "http://localhost:4000",
 		})
+		// Note: huggingface is not fetched in requestRouterModels - it has its own handler
+		// Note: io-intelligence is not fetched because no API key is provided in the mock state
 
 		// Verify response was sent
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
@@ -243,6 +247,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 	})
@@ -333,6 +339,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 	})
@@ -377,6 +385,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				ollama: {},
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				huggingface: {},
+				"io-intelligence": {},
 			},
 		})
 

+ 98 - 42
src/core/webview/webviewMessageHandler.ts

@@ -34,7 +34,7 @@ import { ClineProvider } from "./ClineProvider"
 import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
 import { changeLanguage, t } from "../../i18n"
 import { Package } from "../../shared/package"
-import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
+import { type RouterName, type ModelRecord, toRouterName } from "../../shared/api"
 import { MessageEnhancer } from "./messageEnhancer"
 
 import {
@@ -786,16 +786,19 @@ export const webviewMessageHandler = async (
 		case "requestRouterModels":
 			const { apiConfiguration } = await provider.getState()
 
-			const routerModels: Partial<Record<RouterName, ModelRecord>> = {
+			const routerModels: Record<RouterName, ModelRecord> = {
 				openrouter: {},
-				requesty: {},
-				glama: {},
-				unbound: {},
+				"vercel-ai-gateway": {},
+				huggingface: {},
 				litellm: {},
 				"kilocode-openrouter": {}, // kilocode_change
+				deepinfra: {},
+				"io-intelligence": {},
+				requesty: {},
+				unbound: {},
+				glama: {},
 				ollama: {},
 				lmstudio: {},
-				deepinfra: {},
 			}
 
 			const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@@ -806,7 +809,8 @@ export const webviewMessageHandler = async (
 						`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
 						error,
 					)
-					throw error // Re-throw to be caught by Promise.allSettled
+
+					throw error // Re-throw to be caught by Promise.allSettled.
 				}
 			}
 
@@ -850,8 +854,9 @@ export const webviewMessageHandler = async (
 			]
 			// kilocode_change end
 
-			// Add IO Intelligence if API key is provided
+			// Add IO Intelligence if API key is provided.
 			const ioIntelligenceApiKey = apiConfiguration.ioIntelligenceApiKey
+
 			if (ioIntelligenceApiKey) {
 				modelFetchPromises.push({
 					key: "io-intelligence",
@@ -859,11 +864,12 @@ export const webviewMessageHandler = async (
 				})
 			}
 
-			// Don't fetch Ollama and LM Studio models by default anymore
-			// They have their own specific handlers: requestOllamaModels and requestLmStudioModels
+			// Don't fetch Ollama and LM Studio models by default anymore.
+			// They have their own specific handlers: requestOllamaModels and requestLmStudioModels.
 
 			const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
 			const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
+
 			if (litellmApiKey && litellmBaseUrl) {
 				modelFetchPromises.push({
 					key: "litellm",
@@ -874,24 +880,17 @@ export const webviewMessageHandler = async (
 			const results = await Promise.allSettled(
 				modelFetchPromises.map(async ({ key, options }) => {
 					const models = await safeGetModels(options)
-					return { key, models } // key is RouterName here
+					return { key, models } // The key is `ProviderName` here.
 				}),
 			)
 
-			const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = {
-				...routerModels,
-				// Initialize ollama and lmstudio with empty objects since they use separate handlers
-				ollama: {},
-				lmstudio: {},
-			}
-
 			results.forEach((result, index) => {
-				const routerName = modelFetchPromises[index].key // Get RouterName using index
+				const routerName = modelFetchPromises[index].key
 
 				if (result.status === "fulfilled") {
-					fetchedRouterModels[routerName] = result.value.models
+					routerModels[routerName] = result.value.models
 
-					// Ollama and LM Studio settings pages still need these events
+					// Ollama and LM Studio settings pages still need these events.
 					if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
 						provider.postMessageToWebview({
 							type: "ollamaModels",
@@ -904,11 +903,11 @@ export const webviewMessageHandler = async (
 						})
 					}
 				} else {
-					// Handle rejection: Post a specific error message for this provider
+					// Handle rejection: Post a specific error message for this provider.
 					const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
 					console.error(`Error fetching models for ${routerName}:`, result.reason)
 
-					fetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message
+					routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message.
 
 					provider.postMessageToWebview({
 						type: "singleRouterModelFetchResponse",
@@ -919,30 +918,24 @@ export const webviewMessageHandler = async (
 				}
 			})
 
-			provider.postMessageToWebview({
-				type: "routerModels",
-				routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
-			})
-
+			provider.postMessageToWebview({ type: "routerModels", routerModels })
 			break
 		case "requestOllamaModels": {
-			// Specific handler for Ollama models only
+			// Specific handler for Ollama models only.
 			const { apiConfiguration: ollamaApiConfig } = await provider.getState()
 			try {
-				// Flush cache first to ensure fresh models
+				// Flush cache first to ensure fresh models.
 				await flushModels("ollama")
 
 				const ollamaModels = await getModels({
 					provider: "ollama",
 					baseUrl: ollamaApiConfig.ollamaBaseUrl,
 					apiKey: ollamaApiConfig.ollamaApiKey,
+					numCtx: ollamaApiConfig.ollamaNumCtx, // kilocode_change
 				})
 
 				if (Object.keys(ollamaModels).length > 0) {
-					provider.postMessageToWebview({
-						type: "ollamaModels",
-						ollamaModels: ollamaModels,
-					})
+					provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels })
 				}
 			} catch (error) {
 				// Silently fail - user hasn't configured Ollama yet
@@ -951,10 +944,10 @@ export const webviewMessageHandler = async (
 			break
 		}
 		case "requestLmStudioModels": {
-			// Specific handler for LM Studio models only
+			// Specific handler for LM Studio models only.
 			const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
 			try {
-				// Flush cache first to ensure fresh models
+				// Flush cache first to ensure fresh models.
 				await flushModels("lmstudio")
 
 				const lmStudioModels = await getModels({
@@ -969,7 +962,7 @@ export const webviewMessageHandler = async (
 					})
 				}
 			} catch (error) {
-				// Silently fail - user hasn't configured LM Studio yet
+				// Silently fail - user hasn't configured LM Studio yet.
 				console.debug("LM Studio models fetch failed:", error)
 			}
 			break
@@ -992,19 +985,18 @@ export const webviewMessageHandler = async (
 			provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
 			break
 		case "requestHuggingFaceModels":
+			// TODO: Why isn't this handled by `requestRouterModels` above?
 			try {
 				const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface")
 				const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata()
+
 				provider.postMessageToWebview({
 					type: "huggingFaceModels",
 					huggingFaceModels: huggingFaceModelsResponse.models,
 				})
 			} catch (error) {
 				console.error("Failed to fetch Hugging Face models:", error)
-				provider.postMessageToWebview({
-					type: "huggingFaceModels",
-					huggingFaceModels: [],
-				})
+				provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] })
 			}
 			break
 		case "openImage":
@@ -1131,6 +1123,18 @@ export const webviewMessageHandler = async (
 
 			break
 		}
+		case "openKeyboardShortcuts": {
+			// Open VSCode keyboard shortcuts settings and optionally filter to show the Roo Code commands
+			const searchQuery = message.text || ""
+			if (searchQuery) {
+				// Open with a search query pre-filled
+				await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", searchQuery)
+			} else {
+				// Just open the keyboard shortcuts settings
+				await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings")
+			}
+			break
+		}
 		case "openMcpSettings": {
 			const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()
 
@@ -1740,6 +1744,10 @@ export const webviewMessageHandler = async (
 			await updateGlobalState("historyPreviewCollapsed", message.bool ?? false)
 			// No need to call postStateToWebview here as the UI already updated optimistically
 			break
+		case "setReasoningBlockCollapsed":
+			await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
+			// No need to call postStateToWebview here as the UI already updated optimistically
+			break
 		case "toggleApiConfigPin":
 			if (message.text) {
 				const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
@@ -2754,6 +2762,17 @@ export const webviewMessageHandler = async (
 
 			break
 		}
+		case "cloudLandingPageSignIn": {
+			try {
+				const landingPageSlug = message.text || "supernova"
+				TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
+				await CloudService.instance.login(landingPageSlug)
+			} catch (error) {
+				provider.log(`CloudService#login failed: ${error}`)
+				vscode.window.showErrorMessage("Sign in failed.")
+			}
+			break
+		}
 		case "rooCloudSignOut": {
 			try {
 				await CloudService.instance.logout()
@@ -2808,6 +2827,38 @@ export const webviewMessageHandler = async (
 
 			break
 		}
+		case "switchOrganization": {
+			try {
+				const organizationId = message.organizationId ?? null
+
+				// Switch to the new organization context
+				await CloudService.instance.switchOrganization(organizationId)
+
+				// Refresh the state to update UI
+				await provider.postStateToWebview()
+
+				// Send success response back to webview
+				await provider.postMessageToWebview({
+					type: "organizationSwitchResult",
+					success: true,
+					organizationId: organizationId,
+				})
+			} catch (error) {
+				provider.log(`Organization switch failed: ${error}`)
+				const errorMessage = error instanceof Error ? error.message : String(error)
+
+				// Send error response back to webview
+				await provider.postMessageToWebview({
+					type: "organizationSwitchResult",
+					success: false,
+					error: errorMessage,
+					organizationId: message.organizationId ?? null,
+				})
+
+				vscode.window.showErrorMessage(`Failed to switch organization: ${errorMessage}`)
+			}
+			break
+		}
 
 		case "saveCodeIndexSettingsAtomic": {
 			if (!message.codeIndexSettings) {
@@ -3287,7 +3338,12 @@ export const webviewMessageHandler = async (
 					TelemetryService.instance.captureTabShown(message.tab)
 				}
 
-				await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab })
+				await provider.postMessageToWebview({
+					type: "action",
+					action: "switchTab",
+					tab: message.tab,
+					values: message.values,
+				})
 			}
 			break
 		}

+ 1 - 1
src/extension.ts

@@ -225,7 +225,7 @@ export async function activate(context: vscode.ExtensionContext) {
 	// Add to subscriptions for proper cleanup on deactivate.
 	context.subscriptions.push(cloudService)
 
-	// Trigger initial cloud profile sync now that CloudService is ready
+	// Trigger initial cloud profile sync now that CloudService is ready.
 	try {
 		await provider.initializeCloudProfileSyncWhenReady()
 	} catch (error) {

+ 16 - 0
src/i18n/locales/ar/common.json

@@ -238,9 +238,25 @@
 			"confirm": "احذف"
 		}
 	},
+	"interruption": {
+		"responseInterruptedByUser": "تم مقاطعة الاستجابة من قبل المستخدم",
+		"responseInterruptedByApiError": "تم مقاطعة الاستجابة بسبب خطأ في واجهة برمجة التطبيقات"
+	},
 	"commands": {
 		"preventCompletionWithOpenTodos": {
 			"description": "منع إكمال المهمة عند وجود مهام غير مكتملة في قائمة المهام"
 		}
+	},
+	"docsLink": {
+		"label": "مستندات",
+		"url": "https://docs.roocode.com"
+	},
+	"errorBoundary": {
+		"title": "خطأ",
+		"reportText": "يرجى الإبلاغ عن هذا الخطأ لمساعدتنا في تحسين التطبيق.",
+		"githubText": "إبلاغ على GitHub",
+		"copyInstructions": "انسخ تفاصيل الخطأ أدناه:",
+		"errorStack": "مجموعة الأخطاء",
+		"componentStack": "مجموعة المكونات"
 	}
 }

+ 16 - 0
src/i18n/locales/ca/common.json

@@ -186,6 +186,10 @@
 		"incomplete": "Tasca #{{taskNumber}} (Incompleta)",
 		"no_messages": "Tasca #{{taskNumber}} (Sense missatges)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Resposta interrompuda per l'usuari",
+		"responseInterruptedByApiError": "Resposta interrompuda per error d'API"
+	},
 	"storage": {
 		"prompt_custom_path": "Introdueix una ruta d'emmagatzematge personalitzada per a l'historial de converses o deixa-ho buit per utilitzar la ubicació predeterminada",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Evitar la finalització de tasques quan hi ha todos incomplets a la llista de todos"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Error",
+		"reportText": "Si us plau, informa d'aquest error per ajudar-nos a millorar l'aplicació.",
+		"githubText": "Informar a GitHub",
+		"copyInstructions": "Copia els detalls de l'error a continuació:",
+		"errorStack": "Pila d'errors",
+		"componentStack": "Pila de components"
 	}
 }

+ 16 - 0
src/i18n/locales/cs/common.json

@@ -238,9 +238,25 @@
 			"confirm": "Smazat"
 		}
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Odpověď přerušena uživatelem",
+		"responseInterruptedByApiError": "Odpověď přerušena chybou API"
+	},
 	"commands": {
 		"preventCompletionWithOpenTodos": {
 			"description": "Zabránit dokončení úlohy, když jsou v seznamu úkolů neúplné položky"
 		}
+	},
+	"docsLink": {
+		"label": "Dokumentace",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Chyba",
+		"reportText": "Prosím nahlaste tuto chybu, abychom mohli aplikaci vylepšit.",
+		"githubText": "Nahlásit na GitHub",
+		"copyInstructions": "Zkopírujte podrobnosti chyby níže:",
+		"errorStack": "Zásobník chyb",
+		"componentStack": "Zásobník komponent"
 	}
 }

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

@@ -182,6 +182,10 @@
 		"incomplete": "Aufgabe #{{taskNumber}} (Unvollständig)",
 		"no_messages": "Aufgabe #{{taskNumber}} (Keine Nachrichten)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Antwort vom Benutzer unterbrochen",
+		"responseInterruptedByApiError": "Antwort durch API-Fehler unterbrochen"
+	},
 	"storage": {
 		"prompt_custom_path": "Gib den benutzerdefinierten Speicherpfad für den Gesprächsverlauf ein, leer lassen für Standardspeicherort",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -247,5 +251,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Aufgabenabschluss verhindern, wenn unvollständige Todos in der Todo-Liste vorhanden sind"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Fehler",
+		"reportText": "Bitte melde diesen Fehler, um uns bei der Verbesserung der Anwendung zu helfen.",
+		"githubText": "Auf GitHub melden",
+		"copyInstructions": "Kopiere die unten stehenden Fehlerdetails:",
+		"errorStack": "Fehler-Stack",
+		"componentStack": "Komponenten-Stack"
 	}
 }

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

@@ -182,6 +182,10 @@
 		"incomplete": "Task #{{taskNumber}} (Incomplete)",
 		"no_messages": "Task #{{taskNumber}} (No messages)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Response interrupted by user",
+		"responseInterruptedByApiError": "Response interrupted by API error"
+	},
 	"storage": {
 		"prompt_custom_path": "Enter custom conversation history storage path, leave empty to use default location",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -236,5 +240,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Prevent task completion when there are incomplete todos in the todo list"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Error",
+		"reportText": "Please report this error to help us improve the application.",
+		"githubText": "Report on GitHub",
+		"copyInstructions": "Copy the error details below:",
+		"errorStack": "Error Stack",
+		"componentStack": "Component Stack"
 	}
 }

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

@@ -182,6 +182,10 @@
 		"incomplete": "Tarea #{{taskNumber}} (Incompleta)",
 		"no_messages": "Tarea #{{taskNumber}} (Sin mensajes)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Respuesta interrumpida por el usuario",
+		"responseInterruptedByApiError": "Respuesta interrumpida por error de API"
+	},
 	"storage": {
 		"prompt_custom_path": "Ingresa la ruta de almacenamiento personalizada para el historial de conversaciones, déjala vacía para usar la ubicación predeterminada",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -247,5 +251,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Prevenir la finalización de tareas cuando hay todos incompletos en la lista de todos"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Error",
+		"reportText": "Por favor reporta este error para ayudarnos a mejorar la aplicación.",
+		"githubText": "Reportar en GitHub",
+		"copyInstructions": "Copia los detalles del error a continuación:",
+		"errorStack": "Pila del Error",
+		"componentStack": "Pila de Componentes"
 	}
 }

+ 16 - 0
src/i18n/locales/fr/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Tâche #{{taskNumber}} (Incomplète)",
 		"no_messages": "Tâche #{{taskNumber}} (Aucun message)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Réponse interrompue par l'utilisateur",
+		"responseInterruptedByApiError": "Réponse interrompue par une erreur d'API"
+	},
 	"storage": {
 		"prompt_custom_path": "Entrez le chemin de stockage personnalisé pour l'historique des conversations, laissez vide pour utiliser l'emplacement par défaut",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Empêcher la finalisation des tâches lorsqu'il y a des todos incomplets dans la liste de todos"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Erreur",
+		"reportText": "Veuillez signaler cette erreur pour nous aider à améliorer l'application.",
+		"githubText": "Signaler sur GitHub",
+		"copyInstructions": "Copiez les détails de l'erreur ci-dessous :",
+		"errorStack": "Pile d'erreur",
+		"componentStack": "Pile de composants"
 	}
 }

+ 16 - 0
src/i18n/locales/hi/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "टास्क #{{taskNumber}} (अधूरा)",
 		"no_messages": "टास्क #{{taskNumber}} (कोई संदेश नहीं)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "उपयोगकर्ता द्वारा प्रतिक्रिया बाधित",
+		"responseInterruptedByApiError": "API त्रुटि द्वारा प्रतिक्रिया बाधित"
+	},
 	"storage": {
 		"prompt_custom_path": "वार्तालाप इतिहास के लिए कस्टम स्टोरेज पाथ दर्ज करें, डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ दें",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "जब टूडू सूची में अधूरे टूडू हों तो कार्य पूर्ण होने से रोकें"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "त्रुटि",
+		"reportText": "कृपया इस त्रुटि की रिपोर्ट करें ताकि हम एप्लिकेशन को बेहतर बना सकें।",
+		"githubText": "GitHub पर रिपोर्ट करें",
+		"copyInstructions": "नीचे त्रुटि विवरण कॉपी करें:",
+		"errorStack": "त्रुटि स्टैक",
+		"componentStack": "कॉम्पोनेंट स्टैक"
 	}
 }

+ 16 - 0
src/i18n/locales/id/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Tugas #{{taskNumber}} (Tidak lengkap)",
 		"no_messages": "Tugas #{{taskNumber}} (Tidak ada pesan)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Respons diinterupsi oleh pengguna",
+		"responseInterruptedByApiError": "Respons diinterupsi oleh error API"
+	},
 	"storage": {
 		"prompt_custom_path": "Masukkan path penyimpanan riwayat percakapan kustom, biarkan kosong untuk menggunakan lokasi default",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Mencegah penyelesaian tugas ketika ada todo yang belum selesai dalam daftar todo"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Error",
+		"reportText": "Harap laporkan error ini untuk membantu kami meningkatkan aplikasi.",
+		"githubText": "Laporkan di GitHub",
+		"copyInstructions": "Salin detail error di bawah:",
+		"errorStack": "Stack Error",
+		"componentStack": "Stack Komponen"
 	}
 }

+ 16 - 0
src/i18n/locales/it/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Attività #{{taskNumber}} (Incompleta)",
 		"no_messages": "Attività #{{taskNumber}} (Nessun messaggio)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Risposta interrotta dall'utente",
+		"responseInterruptedByApiError": "Risposta interrotta da errore API"
+	},
 	"storage": {
 		"prompt_custom_path": "Inserisci il percorso di archiviazione personalizzato per la cronologia delle conversazioni, lascia vuoto per utilizzare la posizione predefinita",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Impedire il completamento delle attività quando ci sono todo incompleti nella lista dei todo"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Errore",
+		"reportText": "Segnala questo errore per aiutarci a migliorare l'applicazione.",
+		"githubText": "Segnala su GitHub",
+		"copyInstructions": "Copia i dettagli dell'errore qui sotto:",
+		"errorStack": "Stack dell'errore",
+		"componentStack": "Stack dei componenti"
 	}
 }

+ 16 - 0
src/i18n/locales/ja/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "タスク #{{taskNumber}} (未完了)",
 		"no_messages": "タスク #{{taskNumber}} (メッセージなし)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "ユーザーによって応答が中断されました",
+		"responseInterruptedByApiError": "APIエラーによって応答が中断されました"
+	},
 	"storage": {
 		"prompt_custom_path": "会話履歴のカスタムストレージパスを入力してください。デフォルトの場所を使用する場合は空のままにしてください",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Todoリストに未完了のTodoがある場合、タスクの完了を防ぐ"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "エラー",
+		"reportText": "アプリケーションの改善にご協力いただくため、このエラーを報告してください。",
+		"githubText": "GitHubで報告",
+		"copyInstructions": "以下のエラーの詳細をコピーしてください:",
+		"errorStack": "エラースタック",
+		"componentStack": "コンポーネントスタック"
 	}
 }

+ 16 - 0
src/i18n/locales/ko/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "작업 #{{taskNumber}} (미완료)",
 		"no_messages": "작업 #{{taskNumber}} (메시지 없음)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "사용자에 의해 응답이 중단됨",
+		"responseInterruptedByApiError": "API 오류로 인해 응답이 중단됨"
+	},
 	"storage": {
 		"prompt_custom_path": "대화 내역을 위한 사용자 지정 저장 경로를 입력하세요. 기본 위치를 사용하려면 비워두세요",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "할 일 목록에 미완료된 할 일이 있을 때 작업 완료를 방지"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "오류",
+		"reportText": "애플리케이션 개선에 도움을 주시기 위해 이 오류를 신고해 주세요.",
+		"githubText": "GitHub에서 신고",
+		"copyInstructions": "아래 오류 세부 정보를 복사하세요:",
+		"errorStack": "오류 스택",
+		"componentStack": "컴포넌트 스택"
 	}
 }

+ 16 - 0
src/i18n/locales/nl/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Taak #{{taskNumber}} (Onvolledig)",
 		"no_messages": "Taak #{{taskNumber}} (Geen berichten)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Reactie onderbroken door gebruiker",
+		"responseInterruptedByApiError": "Reactie onderbroken door API-fout"
+	},
 	"storage": {
 		"prompt_custom_path": "Voer een aangepast opslagpad voor gespreksgeschiedenis in, laat leeg voor standaardlocatie",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Voorkom taakafronding wanneer er onvolledige todos in de todolijst staan"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Fout",
+		"reportText": "Meld deze fout om ons te helpen de applicatie te verbeteren.",
+		"githubText": "Melden op GitHub",
+		"copyInstructions": "Kopieer de foutdetails hieronder:",
+		"errorStack": "Foutstack",
+		"componentStack": "Componentenstack"
 	}
 }

+ 16 - 0
src/i18n/locales/pl/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Zadanie #{{taskNumber}} (Niekompletne)",
 		"no_messages": "Zadanie #{{taskNumber}} (Brak wiadomości)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Odpowiedź przerwana przez użytkownika",
+		"responseInterruptedByApiError": "Odpowiedź przerwana przez błąd API"
+	},
 	"storage": {
 		"prompt_custom_path": "Wprowadź niestandardową ścieżkę przechowywania dla historii konwersacji lub pozostaw puste, aby użyć lokalizacji domyślnej",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Zapobiegaj ukończeniu zadania gdy na liście zadań są nieukończone zadania"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Błąd",
+		"reportText": "Zgłoś ten błąd, aby pomóc nam ulepszać aplikację.",
+		"githubText": "Zgłoś na GitHub",
+		"copyInstructions": "Skopiuj szczegóły błędu poniżej:",
+		"errorStack": "Stos błędu",
+		"componentStack": "Stos komponentów"
 	}
 }

+ 16 - 0
src/i18n/locales/pt-BR/common.json

@@ -186,6 +186,10 @@
 		"incomplete": "Tarefa #{{taskNumber}} (Incompleta)",
 		"no_messages": "Tarefa #{{taskNumber}} (Sem mensagens)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Resposta interrompida pelo usuário",
+		"responseInterruptedByApiError": "Resposta interrompida por erro da API"
+	},
 	"storage": {
 		"prompt_custom_path": "Digite o caminho de armazenamento personalizado para o histórico de conversas, deixe em branco para usar o local padrão",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Impedir a conclusão de tarefas quando há todos incompletos na lista de todos"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Erro",
+		"reportText": "Por favor, relate este erro para nos ajudar a melhorar o aplicativo.",
+		"githubText": "Relatar no GitHub",
+		"copyInstructions": "Copie os detalhes do erro abaixo:",
+		"errorStack": "Stack do Erro",
+		"componentStack": "Stack de Componentes"
 	}
 }

+ 16 - 0
src/i18n/locales/ru/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Задача #{{taskNumber}} (Незавершенная)",
 		"no_messages": "Задача #{{taskNumber}} (Нет сообщений)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Ответ прерван пользователем",
+		"responseInterruptedByApiError": "Ответ прерван ошибкой API"
+	},
 	"storage": {
 		"prompt_custom_path": "Введите пользовательский путь хранения истории разговоров, оставьте пустым для использования расположения по умолчанию",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Предотвратить завершение задач при наличии незавершенных дел в списке дел"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Ошибка",
+		"reportText": "Пожалуйста, сообщите об этой ошибке, чтобы помочь нам улучшить приложение.",
+		"githubText": "Сообщить на GitHub",
+		"copyInstructions": "Скопируйте детали ошибки ниже:",
+		"errorStack": "Стек ошибки",
+		"componentStack": "Стек компонентов"
 	}
 }

+ 16 - 0
src/i18n/locales/th/common.json

@@ -238,9 +238,25 @@
 			"confirm": "ลบ"
 		}
 	},
+	"interruption": {
+		"responseInterruptedByUser": "การตอบสนองถูกหยุดโดยผู้ใช้",
+		"responseInterruptedByApiError": "การตอบสนองถูกหยุดโดยข้อผิดพลาดของ API"
+	},
 	"commands": {
 		"preventCompletionWithOpenTodos": {
 			"description": "ป้องกันการทำงานเสร็จสิ้นเมื่อมี todo ที่ยังไม่เสร็จในรายการ todo"
 		}
+	},
+	"docsLink": {
+		"label": "เอกสาร",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "ข้อผิดพลาด",
+		"reportText": "กรุณารายงานข้อผิดพลาดนี้เพื่อช่วยเราปรับปรุงแอปพลิเคชัน",
+		"githubText": "รายงานบน GitHub",
+		"copyInstructions": "คัดลอกรายละเอียดข้อผิดพลาดด้านล่าง:",
+		"errorStack": "Error Stack",
+		"componentStack": "Component Stack"
 	}
 }

+ 16 - 0
src/i18n/locales/tr/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Görev #{{taskNumber}} (Tamamlanmamış)",
 		"no_messages": "Görev #{{taskNumber}} (Mesaj yok)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Yanıt kullanıcı tarafından kesildi",
+		"responseInterruptedByApiError": "Yanıt API hatası nedeniyle kesildi"
+	},
 	"storage": {
 		"prompt_custom_path": "Konuşma geçmişi için özel depolama yolunu girin, varsayılan konumu kullanmak için boş bırakın",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Todo listesinde tamamlanmamış todolar olduğunda görev tamamlanmasını engelle"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Hata",
+		"reportText": "Uygulamayı geliştirmemize yardımcı olmak için lütfen bu hatayı bildirin.",
+		"githubText": "GitHub'da bildir",
+		"copyInstructions": "Aşağıdaki hata ayrıntılarını kopyalayın:",
+		"errorStack": "Hata Stack'i",
+		"componentStack": "Bileşen Stack'i"
 	}
 }

+ 16 - 0
src/i18n/locales/uk/common.json

@@ -238,9 +238,25 @@
 			"confirm": "Видалити"
 		}
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Відповідь перервана користувачем",
+		"responseInterruptedByApiError": "Відповідь перервана помилкою API"
+	},
 	"commands": {
 		"preventCompletionWithOpenTodos": {
 			"description": "Запобігти завершенню завдання, коли є незавершені пункти в списку завдань"
 		}
+	},
+	"docsLink": {
+		"label": "Документація",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Помилка",
+		"reportText": "Будь ласка, повідомте про цю помилку, щоб допомогти нам покращити додаток.",
+		"githubText": "Повідомити на GitHub",
+		"copyInstructions": "Скопіюйте деталі помилки нижче:",
+		"errorStack": "Стек помилки",
+		"componentStack": "Стек компонентів"
 	}
 }

+ 16 - 0
src/i18n/locales/vi/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "Nhiệm vụ #{{taskNumber}} (Chưa hoàn thành)",
 		"no_messages": "Nhiệm vụ #{{taskNumber}} (Không có tin nhắn)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "Phản hồi bị gián đoạn bởi người dùng",
+		"responseInterruptedByApiError": "Phản hồi bị gián đoạn bởi lỗi API"
+	},
 	"storage": {
 		"prompt_custom_path": "Nhập đường dẫn lưu trữ tùy chỉnh cho lịch sử hội thoại, để trống để sử dụng vị trí mặc định",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -259,5 +263,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "Ngăn chặn hoàn thành nhiệm vụ khi có các todo chưa hoàn thành trong danh sách todo"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "Lỗi",
+		"reportText": "Vui lòng báo cáo lỗi này để giúp chúng tôi cải thiện ứng dụng.",
+		"githubText": "Báo cáo trên GitHub",
+		"copyInstructions": "Sao chép chi tiết lỗi bên dưới:",
+		"errorStack": "Stack lỗi",
+		"componentStack": "Stack thành phần"
 	}
 }

+ 16 - 0
src/i18n/locales/zh-CN/common.json

@@ -187,6 +187,10 @@
 		"incomplete": "任务 #{{taskNumber}} (未完成)",
 		"no_messages": "任务 #{{taskNumber}} (无消息)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "响应被用户中断",
+		"responseInterruptedByApiError": "响应被 API 错误中断"
+	},
 	"storage": {
 		"prompt_custom_path": "输入自定义会话历史存储路径,留空以使用默认位置",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -257,5 +261,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "当待办事项列表中有未完成的待办事项时阻止任务完成"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "错误",
+		"reportText": "请报告此错误以帮助我们改进应用程序。",
+		"githubText": "在 GitHub 上报告",
+		"copyInstructions": "复制下方的错误详情:",
+		"errorStack": "错误堆栈",
+		"componentStack": "组件堆栈"
 	}
 }

+ 16 - 0
src/i18n/locales/zh-TW/common.json

@@ -182,6 +182,10 @@
 		"incomplete": "任務 #{{taskNumber}} (未完成)",
 		"no_messages": "任務 #{{taskNumber}} (無訊息)"
 	},
+	"interruption": {
+		"responseInterruptedByUser": "回應被使用者中斷",
+		"responseInterruptedByApiError": "回應被 API 錯誤中斷"
+	},
 	"storage": {
 		"prompt_custom_path": "輸入自定義對話記錄儲存路徑,留空以使用預設位置",
 		"path_placeholder": "D:\\KiloCodeStorage",
@@ -252,5 +256,17 @@
 		"preventCompletionWithOpenTodos": {
 			"description": "當待辦事項清單中有未完成的待辦事項時阻止任務完成"
 		}
+	},
+	"docsLink": {
+		"label": "Docs",
+		"url": "https://kilocode.ai/docs"
+	},
+	"errorBoundary": {
+		"title": "錯誤",
+		"reportText": "請回報此錯誤以協助我們改進應用程式。",
+		"githubText": "在 GitHub 上回報",
+		"copyInstructions": "複製下方的錯誤詳情:",
+		"errorStack": "錯誤堆疊",
+		"componentStack": "元件堆疊"
 	}
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов