Преглед изворни кода

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

Kevin van Dijk пре 8 месеци
родитељ
комит
cc993cbec8
100 измењених фајлова са 4781 додато и 1194 уклоњено
  1. 5 1
      apps/vscode-e2e/src/suite/extension.test.ts
  2. 3 7
      apps/vscode-e2e/src/suite/index.ts
  3. 4 1
      apps/vscode-e2e/src/suite/modes.test.ts
  4. 4 1
      apps/vscode-e2e/src/suite/task.test.ts
  5. 5 0
      apps/vscode-e2e/src/suite/test-utils.ts
  6. 4 7
      apps/vscode-e2e/src/suite/tools/apply-diff.test.ts
  7. 4 7
      apps/vscode-e2e/src/suite/tools/execute-command.test.ts
  8. 4 1
      apps/vscode-e2e/src/suite/tools/insert-content.test.ts
  9. 122 1
      apps/vscode-e2e/src/suite/tools/list-files.test.ts
  10. 4 7
      apps/vscode-e2e/src/suite/tools/read-file.test.ts
  11. 4 4
      apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts
  12. 4 1
      apps/vscode-e2e/src/suite/tools/search-files.test.ts
  13. 5 2
      apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts
  14. 4 1
      apps/vscode-e2e/src/suite/tools/write-to-file.test.ts
  15. 1 1
      apps/web-evals/package.json
  16. 1 1
      apps/web-roo-code/package.json
  17. 66 73
      apps/web-roo-code/src/app/enterprise/page.tsx
  18. 201 110
      apps/web-roo-code/src/app/privacy/page.tsx
  19. 320 0
      apps/web-roo-code/src/app/terms/page.tsx
  20. 8 1
      apps/web-roo-code/src/components/chromes/footer.tsx
  21. 262 107
      packages/cloud/src/AuthService.ts
  22. 31 13
      packages/cloud/src/CloudService.ts
  23. 9 27
      packages/cloud/src/SettingsService.ts
  24. 10 10
      packages/cloud/src/ShareService.ts
  25. 8 0
      packages/cloud/src/__mocks__/vscode.ts
  26. 1094 0
      packages/cloud/src/__tests__/AuthService.spec.ts
  27. 42 9
      packages/cloud/src/__tests__/CloudService.test.ts
  28. 49 35
      packages/cloud/src/__tests__/ShareService.test.ts
  29. 1 1
      packages/types/npm/package.json
  30. 4 0
      packages/types/src/cloud.ts
  31. 1 2
      packages/types/src/experiment.ts
  32. 1 0
      packages/types/src/global-settings.ts
  33. 7 0
      packages/types/src/provider-settings.ts
  34. 13 0
      packages/types/src/providers/claude-code.ts
  35. 3 3
      packages/types/src/providers/gemini.ts
  36. 2 0
      packages/types/src/providers/index.ts
  37. 18 0
      packages/types/src/providers/lm-studio.ts
  38. 17 0
      packages/types/src/providers/ollama.ts
  39. 4 2
      packages/types/src/providers/vertex.ts
  40. 215 18
      pnpm-lock.yaml
  41. 3 0
      src/.vscodeignore
  42. 1 0
      src/__mocks__/fs/promises.ts
  43. 7 1
      src/activate/handleUri.ts
  44. 3 0
      src/api/index.ts
  45. 230 0
      src/api/providers/__tests__/claude-code.spec.ts
  46. 240 0
      src/api/providers/claude-code.ts
  47. 14 0
      src/api/providers/fetchers/__tests__/fixtures/lmstudio-model-details.json
  48. 58 0
      src/api/providers/fetchers/__tests__/fixtures/ollama-model-details.json
  49. 235 0
      src/api/providers/fetchers/__tests__/lmstudio.test.ts
  50. 222 0
      src/api/providers/fetchers/__tests__/ollama.test.ts
  51. 70 0
      src/api/providers/fetchers/lmstudio.ts
  52. 9 0
      src/api/providers/fetchers/modelCache.ts
  53. 100 0
      src/api/providers/fetchers/ollama.ts
  54. 1 0
      src/api/providers/index.ts
  55. 0 15
      src/api/providers/ollama.ts
  56. 7 1
      src/api/transform/stream.ts
  57. 2 0
      src/core/condense/index.ts
  58. 1 1
      src/core/diff/strategies/multi-file-search-replace.ts
  59. 3 6
      src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap
  60. 3 6
      src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap
  61. 3 6
      src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap
  62. 3 6
      src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap
  63. 3 6
      src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap
  64. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap
  65. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap
  66. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap
  67. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap
  68. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap
  69. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap
  70. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap
  71. 3 6
      src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap
  72. 2 1
      src/core/prompts/instructions/create-mcp-server.ts
  73. 1 0
      src/core/prompts/sections/mcp-servers.ts
  74. 1 9
      src/core/prompts/sections/objective.ts
  75. 53 113
      src/core/prompts/tools/__tests__/attempt-completion.spec.ts
  76. 4 30
      src/core/prompts/tools/attempt-completion.ts
  77. 260 1
      src/core/sliding-window/__tests__/sliding-window.spec.ts
  78. 26 2
      src/core/sliding-window/index.ts
  79. 3 0
      src/core/task/Task.ts
  80. 0 356
      src/core/tools/__tests__/attemptCompletionTool.experiment.spec.ts
  81. 2 2
      src/core/tools/__tests__/writeToFileTool.spec.ts
  82. 5 52
      src/core/tools/attemptCompletionTool.ts
  83. 53 12
      src/core/webview/ClineProvider.ts
  84. 125 0
      src/core/webview/__tests__/ClineProvider.spec.ts
  85. 27 7
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  86. 130 21
      src/core/webview/webviewMessageHandler.ts
  87. 20 3
      src/i18n/locales/ca/common.json
  88. 20 3
      src/i18n/locales/de/common.json
  89. 13 1
      src/i18n/locales/en/common.json
  90. 20 3
      src/i18n/locales/es/common.json
  91. 20 3
      src/i18n/locales/fr/common.json
  92. 20 3
      src/i18n/locales/hi/common.json
  93. 25 2
      src/i18n/locales/id/common.json
  94. 20 3
      src/i18n/locales/it/common.json
  95. 20 3
      src/i18n/locales/ja/common.json
  96. 20 3
      src/i18n/locales/ko/common.json
  97. 25 2
      src/i18n/locales/nl/common.json
  98. 20 3
      src/i18n/locales/pl/common.json
  99. 20 3
      src/i18n/locales/pt-BR/common.json
  100. 20 3
      src/i18n/locales/ru/common.json

+ 5 - 1
apps/vscode-e2e/src/suite/extension.test.ts

@@ -1,7 +1,11 @@
 import * as assert from "assert"
 import * as vscode from "vscode"
 
-suite("Kilo Code Extension", () => {
+import { setDefaultSuiteTimeout } from "./test-utils"
+
+suite("Kilo Code Extension", function () {
+	setDefaultSuiteTimeout(this)
+
 	test("Commands should be registered", async () => {
 		const expectedCommands = [
 			"SidebarProvider.open",

+ 3 - 7
apps/vscode-e2e/src/suite/index.ts

@@ -27,13 +27,11 @@ export async function run() {
 
 	globalThis.api = api
 
-	// Configure Mocha with grep pattern if provided
 	const mochaOptions: Mocha.MochaOptions = {
 		ui: "tdd",
-		timeout: 300_000,
+		timeout: 20 * 60 * 1_000, // 20m
 	}
 
-	// Apply grep filter if TEST_GREP is set
 	if (process.env.TEST_GREP) {
 		mochaOptions.grep = process.env.TEST_GREP
 		console.log(`Running tests matching pattern: ${process.env.TEST_GREP}`)
@@ -42,17 +40,16 @@ export async function run() {
 	const mocha = new Mocha(mochaOptions)
 	const cwd = path.resolve(__dirname, "..")
 
-	// Get test files based on filter
 	let testFiles: string[]
+
 	if (process.env.TEST_FILE) {
-		// Run specific test file
 		const specificFile = process.env.TEST_FILE.endsWith(".js")
 			? process.env.TEST_FILE
 			: `${process.env.TEST_FILE}.js`
+
 		testFiles = await glob(`**/${specificFile}`, { cwd })
 		console.log(`Running specific test file: ${specificFile}`)
 	} else {
-		// Run all test files
 		testFiles = await glob("**/**.test.js", { cwd })
 	}
 
@@ -62,7 +59,6 @@ export async function run() {
 
 	testFiles.forEach((testFile) => mocha.addFile(path.resolve(cwd, testFile)))
 
-	// Let's go!
 	return new Promise<void>((resolve, reject) =>
 		mocha.run((failures) => (failures === 0 ? resolve() : reject(new Error(`${failures} tests failed.`)))),
 	)

+ 4 - 1
apps/vscode-e2e/src/suite/modes.test.ts

@@ -1,8 +1,11 @@
 import * as assert from "assert"
 
 import { waitUntilCompleted } from "./utils"
+import { setDefaultSuiteTimeout } from "./test-utils"
+
+suite("Kilo Code Modes", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Kilo Code Modes", () => {
 	test("Should handle switching modes correctly", async () => {
 		const modes: string[] = []
 

+ 4 - 1
apps/vscode-e2e/src/suite/task.test.ts

@@ -3,8 +3,11 @@ import * as assert from "assert"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitUntilCompleted } from "./utils"
+import { setDefaultSuiteTimeout } from "./test-utils"
+
+suite("Kilo Code Task", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Kilo Code Task", () => {
 	test("Should handle prompt and response correctly", async () => {
 		const api = globalThis.api
 

+ 5 - 0
apps/vscode-e2e/src/suite/test-utils.ts

@@ -0,0 +1,5 @@
+export const DEFAULT_SUITE_TIMEOUT = 120_000
+
+export function setDefaultSuiteTimeout(context: Mocha.Suite) {
+	context.timeout(DEFAULT_SUITE_TIMEOUT)
+}

+ 4 - 7
apps/vscode-e2e/src/suite/tools/apply-diff.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code apply_diff Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code apply_diff Tool", () => {
 	let workspaceDir: string
 
 	// Pre-created test files that will be used across tests
@@ -491,9 +494,6 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`,
 	})
 
 	test("Should handle apply_diff errors gracefully", async function () {
-		// Increase timeout for this specific test
-		this.timeout(90_000)
-
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		const testFile = testFiles.errorHandling
@@ -605,9 +605,6 @@ Assume the file exists and you can modify it directly.`,
 	})
 
 	test("Should apply multiple search/replace blocks to edit two separate functions", async function () {
-		// Increase timeout for this specific test
-		this.timeout(60_000)
-
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		const testFile = testFiles.multiSearchReplace

+ 4 - 7
apps/vscode-e2e/src/suite/tools/execute-command.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep, waitUntilCompleted } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code execute_command Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code execute_command Tool", () => {
 	let workspaceDir: string
 
 	// Pre-created test files that will be used across tests
@@ -331,9 +334,6 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`,
 	})
 
 	test("Should execute multiple commands sequentially", async function () {
-		// Increase timeout for this test
-		this.timeout(90_000)
-
 		const api = globalThis.api
 		const testFile = testFiles.multiCommand
 		let taskStarted = false
@@ -447,9 +447,6 @@ After both commands are executed, use the attempt_completion tool to complete th
 	})
 
 	test("Should handle long-running commands", async function () {
-		// Increase timeout for this test
-		this.timeout(60_000)
-
 		const api = globalThis.api
 		let taskStarted = false
 		let _taskCompleted = false

+ 4 - 1
apps/vscode-e2e/src/suite/tools/insert-content.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code insert_content Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code insert_content Tool", () => {
 	let workspaceDir: string
 
 	// Pre-created test files that will be used across tests

+ 122 - 1
apps/vscode-e2e/src/suite/tools/list-files.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code list_files Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code list_files Tool", () => {
 	let workspaceDir: string
 	let testFiles: {
 		rootFile1: string
@@ -387,6 +390,124 @@ This directory contains various files and subdirectories for testing the list_fi
 		}
 	})
 
+	test("Should list symlinked files and directories", async function () {
+		const api = globalThis.api
+		const messages: ClineMessage[] = []
+		let taskCompleted = false
+		let toolExecuted = false
+		let listResults: string | null = null
+
+		// Listen for messages
+		const messageHandler = ({ message }: { message: ClineMessage }) => {
+			messages.push(message)
+
+			// Check for tool execution and capture results
+			if (message.type === "say" && message.say === "api_req_started") {
+				const text = message.text || ""
+				if (text.includes("list_files")) {
+					toolExecuted = true
+					console.log("list_files tool executed (symlinks):", text.substring(0, 200))
+
+					// Extract list results from the tool execution
+					try {
+						const jsonMatch = text.match(/\{"request":".*?"\}/)
+						if (jsonMatch) {
+							const requestData = JSON.parse(jsonMatch[0])
+							if (requestData.request && requestData.request.includes("Result:")) {
+								listResults = requestData.request
+								console.log("Captured symlink test results:", listResults?.substring(0, 300))
+							}
+						}
+					} catch (e) {
+						console.log("Failed to parse symlink test results:", e)
+					}
+				}
+			}
+		}
+		api.on("message", messageHandler)
+
+		// Listen for task completion
+		const taskCompletedHandler = (id: string) => {
+			if (id === taskId) {
+				taskCompleted = true
+			}
+		}
+		api.on("taskCompleted", taskCompletedHandler)
+
+		let taskId: string
+		try {
+			// Create a symlink test directory
+			const testDirName = `symlink-test-${Date.now()}`
+			const testDir = path.join(workspaceDir, testDirName)
+			await fs.mkdir(testDir, { recursive: true })
+
+			// Create a source directory with content
+			const sourceDir = path.join(testDir, "source")
+			await fs.mkdir(sourceDir, { recursive: true })
+			const sourceFile = path.join(sourceDir, "source-file.txt")
+			await fs.writeFile(sourceFile, "Content from symlinked file")
+
+			// Create symlinks to file and directory
+			const symlinkFile = path.join(testDir, "link-to-file.txt")
+			const symlinkDir = path.join(testDir, "link-to-dir")
+
+			try {
+				await fs.symlink(sourceFile, symlinkFile)
+				await fs.symlink(sourceDir, symlinkDir)
+				console.log("Created symlinks successfully")
+			} catch (symlinkError) {
+				console.log("Symlink creation failed (might be platform limitation):", symlinkError)
+				// Skip test if symlinks can't be created
+				console.log("Skipping symlink test - platform doesn't support symlinks")
+				return
+			}
+
+			// Start task to list files in symlink test directory
+			taskId = await api.startNewTask({
+				configuration: {
+					mode: "code",
+					autoApprovalEnabled: true,
+					alwaysAllowReadOnly: true,
+					alwaysAllowReadOnlyOutsideWorkspace: true,
+				},
+				text: `I have created a test directory with symlinks at "${testDirName}". Use the list_files tool to list the contents of this directory. It should show both the original files/directories and the symlinked ones. The directory contains symlinks to both a file and a directory.`,
+			})
+
+			console.log("Symlink test Task ID:", taskId)
+
+			// Wait for task completion
+			await waitFor(() => taskCompleted, { timeout: 60_000 })
+
+			// Verify the list_files tool was executed
+			assert.ok(toolExecuted, "The list_files tool should have been executed")
+
+			// Verify the tool returned results
+			assert.ok(listResults, "Tool execution results should be captured")
+
+			const results = listResults as string
+			console.log("Symlink test results:", results)
+
+			// Check that symlinked items are visible
+			assert.ok(
+				results.includes("link-to-file.txt") || results.includes("source-file.txt"),
+				"Should see either the symlink or the target file",
+			)
+			assert.ok(
+				results.includes("link-to-dir") || results.includes("source/"),
+				"Should see either the symlink or the target directory",
+			)
+
+			console.log("Test passed! Symlinked files and directories are now visible")
+
+			// Cleanup
+			await fs.rm(testDir, { recursive: true, force: true })
+		} finally {
+			// Clean up
+			api.off("message", messageHandler)
+			api.off("taskCompleted", taskCompletedHandler)
+		}
+	})
+
 	test("Should list files in workspace root directory", async function () {
 		const api = globalThis.api
 		const messages: ClineMessage[] = []

+ 4 - 7
apps/vscode-e2e/src/suite/tools/read-file.test.ts

@@ -7,8 +7,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code read_file Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code read_file Tool", () => {
 	let tempDir: string
 	let testFiles: {
 		simple: string
@@ -118,7 +121,6 @@ suite("Roo Code read_file Tool", () => {
 	})
 
 	test("Should read a simple text file", async function () {
-		this.timeout(90_000) // Increase timeout for this test
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskStarted = false
@@ -264,7 +266,6 @@ suite("Roo Code read_file Tool", () => {
 	})
 
 	test("Should read a multiline file", async function () {
-		this.timeout(90_000) // Increase timeout
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskCompleted = false
@@ -376,7 +377,6 @@ suite("Roo Code read_file Tool", () => {
 	})
 
 	test("Should read file with line range", async function () {
-		this.timeout(90_000) // Increase timeout
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskCompleted = false
@@ -562,7 +562,6 @@ suite("Roo Code read_file Tool", () => {
 	})
 
 	test("Should read XML content file", async function () {
-		this.timeout(90_000) // Increase timeout
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskCompleted = false
@@ -634,7 +633,6 @@ suite("Roo Code read_file Tool", () => {
 	})
 
 	test("Should read multiple files in sequence", async function () {
-		this.timeout(90_000) // Increase timeout
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskCompleted = false
@@ -708,7 +706,6 @@ Assume both files exist and you can read them directly. Read each file and tell
 	})
 
 	test("Should read large file efficiently", async function () {
-		this.timeout(90_000) // Increase timeout
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let taskCompleted = false

+ 4 - 4
apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code search_and_replace Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code search_and_replace Tool", () => {
 	let workspaceDir: string
 
 	// Pre-created test files that will be used across tests
@@ -253,9 +256,6 @@ Assume the file exists and you can modify it directly.`,
 	})
 
 	test("Should perform regex pattern replacement", async function () {
-		// Increase timeout for this test
-		this.timeout(90_000)
-
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		const testFile = testFiles.regexReplace

+ 4 - 1
apps/vscode-e2e/src/suite/tools/search-files.test.ts

@@ -6,8 +6,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code search_files Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code search_files Tool", () => {
 	let workspaceDir: string
 	let testFiles: {
 		jsFile: string

+ 5 - 2
apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts

@@ -7,8 +7,11 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code use_mcp_tool Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code use_mcp_tool Tool", () => {
 	let tempDir: string
 	let testFiles: {
 		simple: string
@@ -764,7 +767,7 @@ suite("Roo Code use_mcp_tool Tool", () => {
 		}
 	})
 
-	test("Should validate MCP request message format and complete successfully", async function () {
+	test.skip("Should validate MCP request message format and complete successfully", async function () {
 		const api = globalThis.api
 		const messages: ClineMessage[] = []
 		let _taskCompleted = false

+ 4 - 1
apps/vscode-e2e/src/suite/tools/write-to-file.test.ts

@@ -6,8 +6,11 @@ import * as os from "os"
 import type { ClineMessage } from "@roo-code/types"
 
 import { waitFor, sleep } from "../utils"
+import { setDefaultSuiteTimeout } from "../test-utils"
+
+suite("Roo Code write_to_file Tool", function () {
+	setDefaultSuiteTimeout(this)
 
-suite("Roo Code write_to_file Tool", () => {
 	let tempDir: string
 	let testFilePath: string
 

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

@@ -3,7 +3,7 @@
 	"version": "0.0.0",
 	"type": "module",
 	"scripts": {
-		"lint": "next lint",
+		"lint": "next lint --max-warnings 0",
 		"check-types": "tsc -b",
 		"dev": "scripts/check-services.sh && next dev",
 		"format": "prettier --write src",

+ 1 - 1
apps/web-roo-code/package.json

@@ -3,7 +3,7 @@
 	"version": "0.0.0",
 	"type": "module",
 	"scripts": {
-		"lint": "next lint",
+		"lint": "next lint --max-warnings 0",
 		"check-types": "tsc --noEmit",
 		"dev": "next dev",
 		"build": "next build",

+ 66 - 73
apps/web-roo-code/src/app/enterprise/page.tsx

@@ -16,17 +16,18 @@ export default async function Enterprise() {
 						<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
 							<div>
 								<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl lg:text-6xl">
-									<span className="block">Roo Code for</span>
+									<span className="block">Roo Code Cloud for</span>
 									<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
 										Enterprise
 									</AnimatedText>
 								</h1>
 								<p className="mt-4 max-w-md text-base text-muted-foreground sm:mt-6 sm:text-lg">
-									A next-generation, AI-powered{" "}
+									The{" "}
 									<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
-										coding partner
+										control-plane
 									</AnimatedText>{" "}
-									for enterprise development teams.
+									for AI-powered software development. Gain visibility, governance, and control over
+									your AI coding initiatives.
 								</p>
 							</div>
 							<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
@@ -63,28 +64,28 @@ export default async function Enterprise() {
 							<div className="relative z-10 rounded-lg border border-border bg-card p-6 shadow-lg">
 								<div className="mb-4 flex items-center space-x-2">
 									<Code className="h-6 w-6 text-blue-400" />
-									<h3 className="text-lg font-semibold">Roo Code Enterprise</h3>
+									<h3 className="text-lg font-semibold">Roo Code Cloud Control-Plane</h3>
 								</div>
 								<p className="mb-4 text-sm text-muted-foreground">
-									An AI extension of your team that handles coding tasks, from new code generation to
-									refactoring, bug fixing, and documentation.
+									A unified control system for managing Roo Code across your organization, with the
+									flexibility to extend governance to your broader AI toolkit.
 								</p>
 								<div className="space-y-2">
 									<div className="flex items-center space-x-2">
 										<CheckCircle className="h-4 w-4 text-green-400" />
-										<span className="text-sm">Accelerate development cycles</span>
+										<span className="text-sm">Centralized Roo Code management</span>
 									</div>
 									<div className="flex items-center space-x-2">
 										<CheckCircle className="h-4 w-4 text-green-400" />
-										<span className="text-sm">Enterprise-grade security</span>
+										<span className="text-sm">Real-time usage visibility</span>
 									</div>
 									<div className="flex items-center space-x-2">
 										<CheckCircle className="h-4 w-4 text-green-400" />
-										<span className="text-sm">Custom-tailored to your workflow</span>
+										<span className="text-sm">Enterprise policy enforcement</span>
 									</div>
 									<div className="flex items-center space-x-2">
 										<CheckCircle className="h-4 w-4 text-green-400" />
-										<span className="text-sm">Improve collaboration and onboarding</span>
+										<span className="text-sm">Extensible to other AI tools</span>
 									</div>
 								</div>
 							</div>
@@ -97,10 +98,12 @@ export default async function Enterprise() {
 			<section id="benefits" className="bg-secondary/50 py-16">
 				<div className="container mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="mb-12 text-center">
-						<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Empower Your Development Team</h2>
+						<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
+							Take Control of Your AI Development
+						</h2>
 						<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
-							Roo Code functions like an entire AI dev team embedded in your developers&apos; IDE, ready
-							to accelerate software delivery and improve code quality.
+							Roo Code Cloud provides enterprise-grade control and visibility for Roo Code deployments,
+							with an extensible architecture for your evolving AI strategy.
 						</p>
 					</div>
 
@@ -110,23 +113,23 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Zap className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Accelerate Development Cycles</h3>
+							<h3 className="mb-2 text-xl font-bold">Centralized AI Management Hub</h3>
 							<p className="text-muted-foreground">
-								Supercharge development with AI assistance that helps developers code faster while
-								maintaining quality.
+								Manage Roo Code deployments enterprise-wide, with an extensible platform ready for your
+								broader AI ecosystem.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Faster time-to-market</span>
+									<span>Centralized token management</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>AI pair-programming</span>
+									<span>Multi-model support for Roo</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Improved code quality</span>
+									<span>Extensible architecture</span>
 								</li>
 							</ul>
 						</div>
@@ -136,22 +139,22 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Users className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Augment Your Team with AI Agents</h3>
+							<h3 className="mb-2 text-xl font-bold">Real-Time Usage Visibility</h3>
 							<p className="text-muted-foreground">
-								Roo Code functions like an AI extension of your team, handling various coding tasks.
+								Track Roo Code usage across teams with detailed analytics and cost attribution.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>New code generation</span>
+									<span>Token consumption tracking</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Refactoring and bug fixing</span>
+									<span>Cost attribution by team</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Automate complex migrations</span>
+									<span>AI adoption insights</span>
 								</li>
 							</ul>
 						</div>
@@ -161,22 +164,23 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Shield className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Enterprise-Grade Security</h3>
+							<h3 className="mb-2 text-xl font-bold">Enterprise-Grade Governance</h3>
 							<p className="text-muted-foreground">
-								Keep your data private with on-premises models, keeping proprietary code in-house.
+								Implement security policies for Roo Code that align with your enterprise AI governance
+								framework.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Security and compliance</span>
+									<span>Model allow-lists</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>No external cloud dependencies</span>
+									<span>Data residency controls</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Open-source and extensible</span>
+									<span>Audit trail compliance</span>
 								</li>
 							</ul>
 						</div>
@@ -186,23 +190,23 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Workflow className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Custom-Tailored to Your Workflow</h3>
+							<h3 className="mb-2 text-xl font-bold">5-Minute Control-Plane Setup</h3>
 							<p className="text-muted-foreground">
-								Developers can create Custom Modes for specialized tasks like security auditing or
-								performance tuning.
+								Deploy your Roo Code control-plane instantly with our SaaS solution. No infrastructure
+								required.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Integrate with internal tools</span>
+									<span>Instant deployment</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Adapt to existing workflows</span>
+									<span>SAML/SCIM integration</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Custom AI behaviors</span>
+									<span>REST API access</span>
 								</li>
 							</ul>
 						</div>
@@ -212,23 +216,22 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Users className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Collaboration and Onboarding</h3>
+							<h3 className="mb-2 text-xl font-bold">Manage AI Development Costs</h3>
 							<p className="text-muted-foreground">
-								Ask Mode enables developers to query their codebase in plain language and receive
-								instant answers.
+								Track and control Roo Code costs with detailed analytics and budget controls.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Accelerates onboarding</span>
+									<span>Unified cost visibility</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Improves cross-team collaboration</span>
+									<span>Department chargebacks</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Makes code more accessible</span>
+									<span>Usage optimization</span>
 								</li>
 							</ul>
 						</div>
@@ -238,22 +241,22 @@ export default async function Enterprise() {
 							<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
 								<Zap className="h-6 w-6 text-blue-500" />
 							</div>
-							<h3 className="mb-2 text-xl font-bold">Faster Delivery, Lower Costs</h3>
+							<h3 className="mb-2 text-xl font-bold">Zero Friction for Developers</h3>
 							<p className="text-muted-foreground">
-								Automate routine tasks to accelerate software releases and reduce costs.
+								Developers get seamless Roo Code access while you maintain governance and visibility.
 							</p>
 							<ul className="mt-4 space-y-2">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Improved code quality & consistency</span>
+									<span>Automatic token refresh</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Empowered developers, happier teams</span>
+									<span>Local sidecar architecture</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
-									<span>Rapid knowledge sharing</span>
+									<span>No workflow disruption</span>
 								</li>
 							</ul>
 						</div>
@@ -265,16 +268,15 @@ export default async function Enterprise() {
 			<section className="py-16">
 				<div className="container mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="mb-12 text-center">
-						<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">What Makes Roo Code Unique</h2>
+						<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Why You Need a Control-Plane</h2>
 						<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
-							Unlike traditional code editors or basic autocomplete tools, Roo Code is an autonomous
-							coding agent with powerful capabilities.
+							See how Roo Code Cloud brings enterprise control to AI-powered development.
 						</p>
 					</div>
 
 					<div className="grid gap-8 md:grid-cols-2">
 						<div className="rounded-lg border border-border bg-card p-8 shadow-sm">
-							<h3 className="mb-4 text-2xl font-bold">Traditional AI Coding Assistants</h3>
+							<h3 className="mb-4 text-2xl font-bold">Current State: AI Tool Sprawl</h3>
 							<ul className="space-y-3">
 								<li className="flex items-start">
 									<svg
@@ -289,7 +291,7 @@ export default async function Enterprise() {
 											d="M6 18L18 6M6 6l12 12"
 										/>
 									</svg>
-									<span>Limited to autocomplete and simple suggestions</span>
+									<span>Roo Code tokens managed individually by developers</span>
 								</li>
 								<li className="flex items-start">
 									<svg
@@ -304,7 +306,7 @@ export default async function Enterprise() {
 											d="M6 18L18 6M6 6l12 12"
 										/>
 									</svg>
-									<span>Lack project-wide context understanding</span>
+									<span>No visibility into AI tool usage or costs</span>
 								</li>
 								<li className="flex items-start">
 									<svg
@@ -319,7 +321,7 @@ export default async function Enterprise() {
 											d="M6 18L18 6M6 6l12 12"
 										/>
 									</svg>
-									<span>Can&apos;t execute commands or perform web actions</span>
+									<span>Inconsistent security practices</span>
 								</li>
 								<li className="flex items-start">
 									<svg
@@ -334,7 +336,7 @@ export default async function Enterprise() {
 											d="M6 18L18 6M6 6l12 12"
 										/>
 									</svg>
-									<span>No customization for enterprise workflows</span>
+									<span>Shadow AI spend on corporate cards</span>
 								</li>
 								<li className="flex items-start">
 									<svg
@@ -349,33 +351,33 @@ export default async function Enterprise() {
 											d="M6 18L18 6M6 6l12 12"
 										/>
 									</svg>
-									<span>Often require sending code to external cloud services</span>
+									<span>No centralized governance framework</span>
 								</li>
 							</ul>
 						</div>
 
 						<div className="rounded-lg border border-border bg-card p-8 shadow-sm">
-							<h3 className="mb-4 text-2xl font-bold text-blue-400">Roo Code Enterprise</h3>
+							<h3 className="mb-4 text-2xl font-bold text-blue-400">Roo Code Cloud Control-Plane</h3>
 							<ul className="space-y-3">
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
-									<span>Full-featured AI dev team with natural language communication</span>
+									<span>Centralized Roo Code management dashboard</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
-									<span>Deep understanding of your entire codebase</span>
+									<span>Complete visibility into usage and costs</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
-									<span>Can run tests, execute commands, and perform web actions</span>
+									<span>Consistent policy enforcement</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
-									<span>Custom modes for specialized enterprise tasks</span>
+									<span>Controlled, trackable AI investments</span>
 								</li>
 								<li className="flex items-start">
 									<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
-									<span>On-premises deployment option for data privacy</span>
+									<span>Enterprise-ready governance platform</span>
 								</li>
 							</ul>
 						</div>
@@ -391,8 +393,8 @@ export default async function Enterprise() {
 							Ready to Transform Your Development Process?
 						</h2>
 						<p className="mb-8 text-lg text-muted-foreground">
-							Join our early access program and be among the first to experience the power of Roo Code for
-							Enterprise.
+							Join our early access program and be among the first to experience the power of Roo Code
+							Cloud for Enterprise.
 						</p>
 						<div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
 							<div className="rounded-lg border border-border bg-card p-6 text-center shadow-sm">
@@ -410,15 +412,6 @@ export default async function Enterprise() {
 								<ContactForm formType="demo" buttonText="Contact Us" buttonClassName="w-full" />
 							</div>
 						</div>
-						<div className="mt-8">
-							<Button variant="outline" size="lg">
-								<a
-									href="mailto:[email protected]?subject=Enterprise Guide Request"
-									className="flex items-center justify-center">
-									Download the Enterprise Guide
-								</a>
-							</Button>
-						</div>
 					</div>
 				</div>
 			</section>

+ 201 - 110
apps/web-roo-code/src/app/privacy/page.tsx

@@ -1,175 +1,266 @@
 import { Metadata } from "next"
 
 export const metadata: Metadata = {
-	title: "Privacy Policy - Roo Code Marketing Website",
+	title: "Privacy Policy - Roo Code",
 	description:
-		"Privacy policy for the Roo Code marketing website. Learn how we handle your data and protect your privacy.",
+		"Privacy policy for Roo Code Cloud and marketing website. Learn how we handle your data and protect your privacy.",
 }
 
 export default function Privacy() {
 	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-3xl dark:prose-invert">
+				<div className="prose prose-lg mx-auto max-w-4xl dark:prose-invert">
 					<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
-						Roo Code Marketing Landing Page Privacy Policy
+						Roo Code Cloud Privacy Policy
 					</h1>
-					<p className="text-muted-foreground">Last Updated: March 7th, 2025</p>
+					<p className="text-muted-foreground">Last Updated: June 19, 2025</p>
 
 					<p className="lead">
-						Roo Code respects your privacy and is committed to being transparent about how data is collected
-						and used on our marketing landing page. This policy focuses on data handling for the Roo Code
-						marketing website. For details on how your data is handled within the Roo Code extension itself,
-						please refer to our separate{" "}
-						<a
-							href="https://github.com/RooCodeInc/Roo-Code/blob/main/PRIVACY.md"
-							target="_blank"
-							rel="noopener noreferrer"
-							className="text-primary hover:underline">
-							Roo Code Extension Privacy Policy
-						</a>
-						.
+						This Privacy Policy explains how Roo Code, Inc. (&quot;Roo Code,&quot; &quot;we,&quot;
+						&quot;our,&quot; or &quot;us&quot;) collects, uses, and shares information when you:
 					</p>
-
-					<h2 className="mt-8 text-2xl font-bold">Where Your Data Goes (And Where It Doesn&apos;t)</h2>
-
-					<h3 className="mt-6 text-xl font-bold">Website Analytics & Tracking</h3>
-					<ul>
+					<ul className="lead">
 						<li>
-							We use PostHog (and its standard features) on our marketing landing page to analyze site
-							traffic and usage trends. This collection includes information such as your IP address,
-							browser type, device information, and pages visited.
+							browse any page under <strong>roocode.com</strong> (the <em>Marketing Site</em>); and/or
 						</li>
 						<li>
-							These analytics help us understand how users engage with the website, so we can improve
-							content and design.
+							create an account for, sign in to, or otherwise use <strong>Roo Code Cloud</strong> at{" "}
+							<strong>app.roocode.com</strong> or through the Roo Code extension while authenticated to
+							that Cloud account (the <em>Cloud Service</em>).
 						</li>
-						<li>We do not collect code, project data, or any AI-related prompts on this page.</li>
 					</ul>
 
-					<h3 className="mt-6 text-xl font-bold">Cookies and Similar Technologies</h3>
-					<ul>
-						<li>
-							Our marketing website may use cookies or similar tracking technologies to remember user
-							preferences and provide aggregated analytics.
-						</li>
-						<li>
-							Cookies help with things like user session management, remembering certain selections or
-							preferences, and compiling anonymous statistics.
-						</li>
-					</ul>
+					<div className="my-8 rounded-lg border border-border bg-muted/50 p-6">
+						<h3 className="mt-0 text-lg font-semibold">Extension‑Only Usage</h3>
+						<p className="mb-0">
+							If you run the Roo Code extension <strong>without</strong> connecting to a Cloud account,
+							your data is governed by the standalone{" "}
+							<a
+								href="https://github.com/RooCodeInc/Roo-Code/blob/main/PRIVACY.md"
+								target="_blank"
+								rel="noopener noreferrer"
+								className="text-primary hover:underline">
+								Roo Code Extension Privacy Policy
+							</a>
+							.
+						</p>
+					</div>
 
-					<h3 className="mt-6 text-xl font-bold">Forms & Voluntary Submissions</h3>
+					<h2 className="mt-12 text-2xl font-bold">Quick Summary</h2>
 					<ul>
 						<li>
-							If you submit your email or other personal data on our landing page (for example, to receive
-							updates or join a waiting list), we collect that information voluntarily provided by you.
+							<strong>Your source code never transits Roo Code servers.</strong> It stays on your device
+							and is sent <strong>directly</strong>—via a client‑to‑provider TLS connection—to the
+							third‑party AI model you select. Roo Code never stores, inspects, or trains on your code.
 						</li>
 						<li>
-							We do not share or sell this data to third parties for their own marketing purposes. It is
-							used only to communicate with you about Roo Code, respond to inquiries, or send updates
-							you&apos;ve requested.
+							<strong>Prompts and chat snippets are collected by default</strong> in Roo Code Cloud so you
+							can search and re‑use past conversations. Organization admins can disable this collection at
+							any time.
 						</li>
-					</ul>
-
-					<h3 className="mt-6 text-xl font-bold">Third-Party Integrations</h3>
-					<ul>
 						<li>
-							Our website may embed content or links to external platforms (e.g., for processing payments
-							or handling support). Any data you provide through these external sites is governed by the
-							privacy policies of those platforms.
+							We collect only the data needed to operate Roo Code Cloud, do <strong>not</strong> sell
+							customer data, and do <strong>not</strong> use your content to train models.
 						</li>
 					</ul>
 
-					<h2 className="mt-8 text-2xl font-bold">How We Use Your Data</h2>
+					<h2 className="mt-12 text-2xl font-bold">1. Information We Collect</h2>
+
+					<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-2 text-left font-semibold">Category</th>
+									<th className="border border-border px-4 py-2 text-left font-semibold">Examples</th>
+									<th className="border border-border px-4 py-2 text-left font-semibold">Source</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Account Information</td>
+									<td className="border border-border px-4 py-2">
+										Name, email, organization, auth tokens
+									</td>
+									<td className="border border-border px-4 py-2">You</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">
+										Workspace Configuration
+									</td>
+									<td className="border border-border px-4 py-2">
+										Org settings, allow‑lists, rules files, modes, dashboards
+									</td>
+									<td className="border border-border px-4 py-2">You / Extension (when signed in)</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Prompts, Chat Snippets & Token Counts
+									</td>
+									<td className="border border-border px-4 py-2">
+										Text prompts, model outputs, token counts
+									</td>
+									<td className="border border-border px-4 py-2">Extension (when signed in)</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Usage Data</td>
+									<td className="border border-border px-4 py-2">
+										Feature clicks, error logs, performance metrics (captured via PostHog)
+									</td>
+									<td className="border border-border px-4 py-2">Services automatically (PostHog)</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Payment Data</td>
+									<td className="border border-border px-4 py-2">
+										Tokenized card details, billing address, invoices
+									</td>
+									<td className="border border-border px-4 py-2">Payment processor (Stripe)</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Marketing Data</td>
+									<td className="border border-border px-4 py-2">
+										Cookies, IP address, browser type, page views,{" "}
+										<strong>voluntary form submissions</strong> (e.g., newsletter or wait‑list
+										sign‑ups)
+									</td>
+									<td className="border border-border px-4 py-2">
+										Marketing Site automatically / You
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
 
-					<h3 className="mt-6 text-xl font-bold">Site Improvements & Marketing</h3>
+					<h2 className="mt-12 text-2xl font-bold">2. How We Use Information</h2>
 					<ul>
 						<li>
-							We analyze aggregated user behavior to measure the effectiveness of our site, troubleshoot
-							any issues, and guide future improvements.
+							<strong>Operate & secure Roo Code Cloud</strong> (authentication, completions, abuse
+							prevention)
 						</li>
 						<li>
-							If you sign up for newsletters or updates, we use your email or other contact information
-							only to send you relevant Roo Code communications.
+							<strong>Provide support & improve features</strong> (debugging, analytics, product
+							decisions)
 						</li>
-					</ul>
-
-					<h3 className="mt-6 text-xl font-bold">No Selling or Sharing of Data</h3>
-					<ul>
 						<li>
-							We do not sell or share your personally identifiable information with third parties for
-							their marketing.
+							<strong>Process payments & manage subscriptions</strong>
+						</li>
+						<li>
+							<strong>Send product updates and roadmap communications</strong> (opt‑out available)
 						</li>
-						<li>We do not train any models on your marketing site data.</li>
 					</ul>
 
-					<h2 className="mt-8 text-2xl font-bold">Your Choices & Control</h2>
+					<h2 className="mt-12 text-2xl font-bold">3. Where Your Data Goes (And Doesn&apos;t)</h2>
+
+					<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-2 text-left font-semibold">Data</th>
+									<th className="border border-border px-4 py-2 text-left font-semibold">Sent To</th>
+									<th className="border border-border px-4 py-2 text-left font-semibold">
+										<strong>Not</strong> Sent To
+									</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Code & files you work on
+									</td>
+									<td className="border border-border px-4 py-2">
+										Your chosen model provider (direct client → provider TLS)
+									</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code servers; ad networks; model‑training pipelines
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">
+										Prompts, chat snippets & token counts (Cloud)
+									</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code Cloud (encrypted at rest)
+									</td>
+									<td className="border border-border px-4 py-2">Any third‑party</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Workspace Configuration
+									</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code Cloud (encrypted at rest)
+									</td>
+									<td className="border border-border px-4 py-2">Any third-party</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Usage & Telemetry</td>
+									<td className="border border-border px-4 py-2">
+										PostHog (self‑hosted analytics platform)
+									</td>
+									<td className="border border-border px-4 py-2">Ad networks or data brokers</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Payment Data</td>
+									<td className="border border-border px-4 py-2">Stripe (PCI‑DSS Level 1)</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code servers (we store only the Stripe customer ID)
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
 
-					<h3 className="mt-6 text-xl font-bold">Manage Cookies</h3>
+					<h2 className="mt-12 text-2xl font-bold">4. Data Retention</h2>
 					<ul>
 						<li>
-							Most browsers allow you to manage or block cookies. If you disable cookies, some features of
-							the site may not function properly.
+							<strong>Source Code:</strong> Never stored on Roo Code servers.
 						</li>
-					</ul>
-
-					<h3 className="mt-6 text-xl font-bold">Opt-Out of Communications</h3>
-					<ul>
 						<li>
-							If you have signed up to receive updates, you can unsubscribe anytime by following the
-							instructions in our emails or contacting us directly.
+							<strong>Prompts & Chat Snippets:</strong> Persist in your Cloud workspace until you or your
+							organization admin deletes them or disables collection.
 						</li>
-					</ul>
-
-					<h3 className="mt-6 text-xl font-bold">Request Deletion</h3>
-					<ul>
 						<li>
-							You may request the deletion of any personal data you&apos;ve provided through our marketing
-							forms by reaching out to us at{" "}
-							<a href="mailto:[email protected]" className="text-primary hover:underline">
-								[email protected]
-							</a>
-							.
+							<strong>Operational Logs & Analytics:</strong> Retained only as needed to operate and secure
+							Roo Code Cloud.
 						</li>
 					</ul>
 
-					<h2 className="mt-8 text-2xl font-bold">Security & Updates</h2>
+					<h2 className="mt-12 text-2xl font-bold">5. Your Choices</h2>
 					<ul>
 						<li>
-							We take reasonable measures to protect your data from unauthorized access or disclosure, but
-							no website can be 100% secure.
+							<strong>Manage cookies:</strong> You can block or delete cookies in your browser settings;
+							some site features may not function without them.
 						</li>
 						<li>
-							If our privacy practices for the marketing site change, we will update this policy and note
-							the effective date at the top.
+							<strong>Disable prompt collection</strong> in Organization settings.
+						</li>
+						<li>
+							<strong>Delete your Cloud account</strong> at any time from{" "}
+							<strong>Security Settings</strong> inside Roo Code Cloud.
 						</li>
 					</ul>
 
-					<h2 className="mt-8 text-2xl font-bold">Contact Us</h2>
+					<h2 className="mt-12 text-2xl font-bold">6. Security Practices</h2>
 					<p>
-						If you have questions or concerns about this Privacy Policy or wish to make a request regarding
-						your data, please reach out to us at{" "}
-						<a href="mailto:[email protected]" className="text-primary hover:underline">
-							[email protected]
+						We use TLS for all data in transit, AES‑256 encryption at rest, least‑privilege IAM, continuous
+						monitoring, routine penetration testing, and maintain a SOC 2 program.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">7. Updates to This Policy</h2>
+					<p>
+						If our privacy practices change, we will update this policy and note the new{" "}
+						<strong>Last Updated</strong> date at the top. For material changes that affect Cloud
+						workspaces, we will also email registered workspace owners before the changes take effect.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">8. Contact Us</h2>
+					<p>
+						Questions or concerns? Email{" "}
+						<a href="mailto:[email protected]" className="text-primary hover:underline">
+							[email protected]
 						</a>
 						.
 					</p>
-
-					<div className="mt-8 border-t border-border pt-6">
-						<p className="text-muted-foreground">
-							By using the Roo Code marketing landing page, you agree to this Privacy Policy. If you use
-							the Roo Code extension, please see our separate{" "}
-							<a
-								href="https://github.com/RooCodeInc/Roo-Code/blob/main/PRIVACY.md"
-								target="_blank"
-								rel="noopener noreferrer"
-								className="text-primary hover:underline">
-								Roo Code Extension Privacy Policy
-							</a>{" "}
-							for details on data handling in the extension.
-						</p>
-					</div>
 				</div>
 			</div>
 		</>

+ 320 - 0
apps/web-roo-code/src/app/terms/page.tsx

@@ -0,0 +1,320 @@
+import { Metadata } from "next"
+
+export const metadata: Metadata = {
+	title: "Terms of Service - Roo Code",
+	description:
+		"Terms of Service for Roo Code Cloud. Learn about our service terms, commercial conditions, and legal framework.",
+}
+
+export default function Terms() {
+	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">
+					<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
+						Roo Code Cloud Terms of Service
+					</h1>
+					<p className="text-muted-foreground">
+						<em>(Version 1.0 – Effective June 19, 2025)</em>
+					</p>
+
+					<p className="lead">
+						These Terms of Service (&quot;<strong>TOS</strong>&quot;) govern access to and use of the Roo
+						Code Cloud service (the &quot;<strong>Service</strong>&quot;). They apply to:
+					</p>
+					<ul className="lead">
+						<li>
+							<strong>(a)</strong> every <strong>Sales Order Form</strong> or similar document mutually
+							executed by Roo Code and the customer that references these TOS; <strong>and</strong>
+						</li>
+						<li>
+							<strong>(b)</strong> any{" "}
+							<strong>online plan-selection, self-service sign-up, or in-app purchase flow</strong>{" "}
+							through which a customer clicks an &quot;I Agree&quot; (or equivalent) button to accept
+							these TOS — such flow also being an <strong>&quot;Order Form.&quot;</strong>
+						</li>
+					</ul>
+
+					<p>
+						By <strong>creating an account, clicking to accept, or using the Service</strong>, the person or
+						entity doing so (&quot;<strong>Customer</strong>&quot;) agrees to be bound by these TOS, even if
+						no separate Order Form is signed.
+					</p>
+
+					<p>
+						If Roo Code and Customer later execute a Master Subscription Agreement (&quot;
+						<strong>MSA</strong>&quot;), the MSA governs; otherwise, these TOS and the applicable Order Form
+						together form the entire agreement (the &quot;<strong>Agreement</strong>&quot;).
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">1. Agreement Framework</h2>
+					<ol>
+						<li>
+							<strong>Incorporation of Standard Terms.</strong>
+							<br />
+							The{" "}
+							<a
+								href="https://commonpaper.com/standards/cloud-service-agreement/2.0/"
+								target="_blank"
+								rel="noopener noreferrer"
+								className="text-primary hover:underline">
+								<em>Common Paper Cloud Service Standard Terms v 2.0</em>
+							</a>{" "}
+							(the &quot;<strong>Standard Terms</strong>&quot;) are incorporated by reference. If these
+							TOS conflict with the Standard Terms, these TOS control.
+						</li>
+						<li>
+							<strong>Order of Precedence.</strong>
+							<br />
+							(a) Order Form (b) these TOS (c) Standard Terms.
+						</li>
+					</ol>
+
+					<h2 className="mt-12 text-2xl font-bold">2. Key Commercial Terms</h2>
+
+					<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-2 text-left font-semibold">Term</th>
+									<th className="border border-border px-4 py-2 text-left font-semibold">Value</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Governing Law / Forum
+									</td>
+									<td className="border border-border px-4 py-2">
+										Delaware law; exclusive jurisdiction and venue in the state or federal courts
+										located in Delaware
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">
+										Plans & Subscription Periods
+									</td>
+									<td className="border border-border px-4 py-2">
+										<em>Free Plan:</em> month-to-month.
+										<br />
+										<em>Paid Plans:</em> Monthly <strong>or</strong> Annual, as selected in an Order
+										Form or the online flow.
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Auto-Renewal & Non-Renewal Notice
+									</td>
+									<td className="border border-border px-4 py-2">
+										<em>Free Plan:</em> renews continuously until cancelled in the dashboard.
+										<br />
+										<em>Paid Plans:</em> renew for the same period unless either party gives 30
+										days&apos; written notice before the current period ends.
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Fees & Usage</td>
+									<td className="border border-border px-4 py-2">
+										<em>Free Plan:</em> Subscription Fee = $0.
+										<br />
+										<em>Paid Plans:</em> Fees stated in the Order Form or online checkout{" "}
+										<strong>plus</strong> usage-based fees, calculated and invoiced monthly.
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Payment Terms</td>
+									<td className="border border-border px-4 py-2">
+										<em>Monthly paid plans:</em> credit-card charge on the billing date.
+										<br />
+										<em>Annual paid plans:</em> invoiced Net 30 (credit card optional).
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">
+										General Liability Cap
+									</td>
+									<td className="border border-border px-4 py-2">
+										The greater of (i) USD 100 and (ii) 1 × Fees paid or payable in the 12 months
+										before the event giving rise to liability.
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Increased Cap / Unlimited Claims
+									</td>
+									<td className="border border-border px-4 py-2">None</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Trial / Pilot</td>
+									<td className="border border-border px-4 py-2">Not offered</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Beta Features</td>
+									<td className="border border-border px-4 py-2">
+										None – only generally available features are provided
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">Security Standard</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code maintains commercially reasonable administrative, physical, and
+										technical safeguards
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">Machine-Learning Use</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code <strong>does not</strong> use Customer Content to train, fine-tune, or
+										improve any ML or AI models
+									</td>
+								</tr>
+								<tr className="bg-muted/25">
+									<td className="border border-border px-4 py-2 font-medium">
+										Data Processing Addendum (DPA)
+									</td>
+									<td className="border border-border px-4 py-2">
+										GDPR/CCPA-ready DPA available upon written request
+									</td>
+								</tr>
+								<tr>
+									<td className="border border-border px-4 py-2 font-medium">
+										Publicity / Logo Rights
+									</td>
+									<td className="border border-border px-4 py-2">
+										Roo Code may identify Customer (name & logo) in marketing materials unless
+										Customer opts out in writing
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+
+					<h2 className="mt-12 text-2xl font-bold">3. Modifications to the Standard Terms</h2>
+					<ol>
+						<li>
+							<strong>Section 1.6 (Machine Learning).</strong>
+							<br />
+							&quot;Provider will not use Customer Content or Usage Data to train, fine-tune, or improve
+							any machine-learning or AI model, except with Customer&apos;s prior written consent.&quot;
+						</li>
+						<li>
+							<strong>Section 3 (Security).</strong>
+							<br />
+							Replace &quot;reasonable&quot; with &quot;commercially reasonable.&quot;
+						</li>
+						<li>
+							<strong>Section 4 (Fees & Payment).</strong>
+							<br />
+							Add usage-billing language above and delete any provision allowing unilateral fee increases.
+						</li>
+						<li>
+							<strong>Section 5 (Term & Termination).</strong>
+							<br />
+							Insert auto-renewal and free-plan language above.
+						</li>
+						<li>
+							<strong>Sections 7 (Trials / Betas) and any SLA references.</strong>
+							<br />
+							Deleted – Roo Code offers no trials, pilots, betas, or SLA credits under these TOS.
+						</li>
+						<li>
+							<strong>Section 12.12 (Publicity).</strong>
+							<br />
+							As reflected in the &quot;Publicity / Logo Rights&quot; row above.
+						</li>
+					</ol>
+
+					<h2 className="mt-12 text-2xl font-bold">4. Use of the Service</h2>
+					<p>
+						Customer may access and use the Service solely for its internal business purposes and subject to
+						the Acceptable Use Policy in the Standard Terms.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">5. Account Management & Termination</h2>
+					<ul>
+						<li>
+							<strong>Self-service cancellation or downgrade.</strong>
+							<br />
+							Customer may cancel a Free Plan immediately, or cancel/downgrade a Paid Plan effective at
+							the end of the current billing cycle, via the web dashboard.
+						</li>
+						<li>
+							Either party may otherwise terminate the Agreement as allowed under Section 5 of the
+							Standard Terms.
+						</li>
+					</ul>
+
+					<h2 className="mt-12 text-2xl font-bold">6. Privacy & Data</h2>
+					<p>
+						Roo Code&apos;s Privacy Notice (
+						<a
+							href="https://roocode.com/privacy"
+							rel="noopener noreferrer"
+							className="text-primary hover:underline">
+							https://roocode.com/privacy
+						</a>
+						) explains how Roo Code collects and handles personal information. If Customer requires a DPA,
+						email{" "}
+						<a href="mailto:[email protected]" className="text-primary hover:underline">
+							[email protected]
+						</a>
+						.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">7. Warranty Disclaimer</h2>
+					<p>
+						Except as expressly stated in the Agreement, the Service is provided{" "}
+						<strong>&quot;as is,&quot;</strong> and all implied warranties are disclaimed to the maximum
+						extent allowed by law.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">8. Limitation of Liability</h2>
+					<p>
+						The caps in Section 2 apply to all claims under the Agreement, whether in contract, tort, or
+						otherwise, except for Excluded Claims defined in the Standard Terms.
+					</p>
+
+					<h2 className="mt-12 text-2xl font-bold">9. Miscellaneous</h2>
+					<ol>
+						<li>
+							<strong>Assignment.</strong>
+							<br />
+							Customer may not assign the Agreement without Roo Code&apos;s prior written consent, except
+							to a successor in a merger or sale of substantially all assets.
+						</li>
+						<li>
+							<strong>Export Compliance.</strong>
+							<br />
+							Each party will comply with all applicable export-control laws and regulations and will not
+							export or re-export any software or technical data without the required government licences.
+						</li>
+						<li>
+							<strong>Entire Agreement.</strong>
+							<br />
+							The Agreement supersedes all prior or contemporaneous agreements for the Service.
+						</li>
+						<li>
+							<strong>Amendments.</strong>
+							<br />
+							Roo Code may update these TOS by posting a revised version at the same URL and emailing or
+							in-app notifying Customer at least 30 days before changes take effect. Continued use after
+							the effective date constitutes acceptance.
+						</li>
+					</ol>
+
+					<h2 className="mt-12 text-2xl font-bold">10. Contact</h2>
+					<p>
+						<strong>Roo Code, Inc.</strong>
+						<br />
+						98 Graceland Dr, San Rafael, CA 94901 USA
+						<br />
+						Email:{" "}
+						<a href="mailto:[email protected]" className="text-primary hover:underline">
+							[email protected]
+						</a>
+					</p>
+				</div>
+			</div>
+		</>
+	)
+}

+ 8 - 1
apps/web-roo-code/src/components/chromes/footer.tsx

@@ -246,6 +246,13 @@ export function Footer() {
 											Careers
 										</a>
 									</li>
+									<li>
+										<Link
+											href="/terms"
+											className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
+											Terms of Service
+										</Link>
+									</li>
 									<li>
 										<div className="relative z-10" ref={dropdownRef}>
 											<button
@@ -276,7 +283,7 @@ export function Footer() {
 															href={INTERNAL_LINKS.PRIVACY_POLICY_WEBSITE}
 															onClick={() => setPrivacyDropdownOpen(false)}
 															className="rounded-md px-3 py-2 transition-colors hover:bg-accent/50 hover:text-foreground">
-															Marketing Website
+															Roo Code Cloud
 														</Link>
 													</div>
 												</div>

+ 262 - 107
packages/cloud/src/AuthService.ts

@@ -1,17 +1,17 @@
 import crypto from "crypto"
 import EventEmitter from "events"
 
-import axios from "axios"
 import * as vscode from "vscode"
 import { z } from "zod"
 
 import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"
 
-import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
+import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "./Config"
 import { RefreshTimer } from "./RefreshTimer"
 import { getUserAgent } from "./utils"
 
 export interface AuthServiceEvents {
+	"attempting-session": [data: { previousState: AuthState }]
 	"inactive-session": [data: { previousState: AuthState }]
 	"active-session": [data: { previousState: AuthState }]
 	"logged-out": [data: { previousState: AuthState }]
@@ -21,24 +21,81 @@ export interface AuthServiceEvents {
 const authCredentialsSchema = z.object({
 	clientToken: z.string().min(1, "Client token cannot be empty"),
 	sessionId: z.string().min(1, "Session ID cannot be empty"),
+	organizationId: z.string().nullable().optional(),
 })
 
 type AuthCredentials = z.infer<typeof authCredentialsSchema>
 
-const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials"
 const AUTH_STATE_KEY = "clerk-auth-state"
 
-type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
+type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session"
+
+const clerkSignInResponseSchema = z.object({
+	response: z.object({
+		created_session_id: z.string(),
+	}),
+})
+
+const clerkCreateSessionTokenResponseSchema = z.object({
+	jwt: z.string(),
+})
+
+const clerkMeResponseSchema = z.object({
+	response: z.object({
+		first_name: z.string().optional(),
+		last_name: z.string().optional(),
+		image_url: z.string().optional(),
+		primary_email_address_id: z.string().optional(),
+		email_addresses: z
+			.array(
+				z.object({
+					id: z.string(),
+					email_address: z.string(),
+				}),
+			)
+			.optional(),
+	}),
+})
+
+const clerkOrganizationMembershipsSchema = z.object({
+	response: z.array(
+		z.object({
+			id: z.string(),
+			role: z.string(),
+			permissions: z.array(z.string()).optional(),
+			created_at: z.number().optional(),
+			updated_at: z.number().optional(),
+			organization: z.object({
+				id: z.string(),
+				name: z.string(),
+				slug: z.string().optional(),
+				image_url: z.string().optional(),
+				has_image: z.boolean().optional(),
+				created_at: z.number().optional(),
+				updated_at: z.number().optional(),
+			}),
+		}),
+	),
+})
+
+class InvalidClientTokenError extends Error {
+	constructor() {
+		super("Invalid/Expired client token")
+		Object.setPrototypeOf(this, InvalidClientTokenError.prototype)
+	}
+}
 
 export class AuthService extends EventEmitter<AuthServiceEvents> {
 	private context: vscode.ExtensionContext
 	private timer: RefreshTimer
 	private state: AuthState = "initializing"
 	private log: (...args: unknown[]) => void
+	private readonly authCredentialsKey: string
 
 	private credentials: AuthCredentials | null = null
 	private sessionToken: string | null = null
 	private userInfo: CloudUserInfo | null = null
+	private isFirstRefreshAttempt: boolean = false
 
 	constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
 		super()
@@ -46,6 +103,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 		this.context = context
 		this.log = log || console.log
 
+		// Calculate auth credentials key based on Clerk base URL
+		const clerkBaseUrl = getClerkBaseUrl()
+		if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) {
+			this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}`
+		} else {
+			this.authCredentialsKey = "clerk-auth-credentials"
+		}
+
 		this.timer = new RefreshTimer({
 			callback: async () => {
 				await this.refreshSession()
@@ -67,7 +132,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 					this.credentials.clientToken !== credentials.clientToken ||
 					this.credentials.sessionId !== credentials.sessionId
 				) {
-					this.transitionToInactiveSession(credentials)
+					this.transitionToAttemptingSession(credentials)
 				}
 			} else {
 				if (this.state !== "logged-out") {
@@ -94,19 +159,32 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 		this.log("[auth] Transitioned to logged-out state")
 	}
 
-	private transitionToInactiveSession(credentials: AuthCredentials): void {
+	private transitionToAttemptingSession(credentials: AuthCredentials): void {
 		this.credentials = credentials
 
 		const previousState = this.state
-		this.state = "inactive-session"
+		this.state = "attempting-session"
 
 		this.sessionToken = null
 		this.userInfo = null
+		this.isFirstRefreshAttempt = true
 
-		this.emit("inactive-session", { previousState })
+		this.emit("attempting-session", { previousState })
 
 		this.timer.start()
 
+		this.log("[auth] Transitioned to attempting-session state")
+	}
+
+	private transitionToInactiveSession(): void {
+		const previousState = this.state
+		this.state = "inactive-session"
+
+		this.sessionToken = null
+		this.userInfo = null
+
+		this.emit("inactive-session", { previousState })
+
 		this.log("[auth] Transitioned to inactive-session state")
 	}
 
@@ -126,7 +204,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 
 		this.context.subscriptions.push(
 			this.context.secrets.onDidChange((e) => {
-				if (e.key === AUTH_CREDENTIALS_KEY) {
+				if (e.key === this.authCredentialsKey) {
 					this.handleCredentialsChange()
 				}
 			}),
@@ -134,16 +212,25 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 	}
 
 	private async storeCredentials(credentials: AuthCredentials): Promise<void> {
-		await this.context.secrets.store(AUTH_CREDENTIALS_KEY, JSON.stringify(credentials))
+		await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials))
 	}
 
 	private async loadCredentials(): Promise<AuthCredentials | null> {
-		const credentialsJson = await this.context.secrets.get(AUTH_CREDENTIALS_KEY)
+		const credentialsJson = await this.context.secrets.get(this.authCredentialsKey)
 		if (!credentialsJson) return null
 
 		try {
 			const parsedJson = JSON.parse(credentialsJson)
-			return authCredentialsSchema.parse(parsedJson)
+			const credentials = authCredentialsSchema.parse(parsedJson)
+
+			// Migration: If no organizationId but we have userInfo, add it
+			if (credentials.organizationId === undefined && this.userInfo?.organizationId) {
+				credentials.organizationId = this.userInfo.organizationId
+				await this.storeCredentials(credentials)
+				this.log("[auth] Migrated credentials with organizationId")
+			}
+
+			return credentials
 		} catch (error) {
 			if (error instanceof z.ZodError) {
 				this.log("[auth] Invalid credentials format:", error.errors)
@@ -155,7 +242,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 	}
 
 	private async clearCredentials(): Promise<void> {
-		await this.context.secrets.delete(AUTH_CREDENTIALS_KEY)
+		await this.context.secrets.delete(this.authCredentialsKey)
 	}
 
 	/**
@@ -192,8 +279,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 	 *
 	 * @param code The authorization code from the callback
 	 * @param state The state parameter from the callback
+	 * @param organizationId The organization ID from the callback (null for personal accounts)
 	 */
-	public async handleCallback(code: string | null, state: string | null): Promise<void> {
+	public async handleCallback(
+		code: string | null,
+		state: string | null,
+		organizationId?: string | null,
+	): Promise<void> {
 		if (!code || !state) {
 			vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url")
 			return
@@ -208,7 +300,10 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 				throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
 			}
 
-			const { credentials } = await this.clerkSignIn(code)
+			const credentials = await this.clerkSignIn(code)
+
+			// Set organizationId (null for personal accounts)
+			credentials.organizationId = organizationId || null
 
 			await this.storeCredentials(credentials)
 
@@ -267,16 +362,27 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 	/**
 	 * Check if the user is authenticated
 	 *
-	 * @returns True if the user is authenticated (has an active or inactive session)
+	 * @returns True if the user is authenticated (has an active, attempting, or inactive session)
 	 */
 	public isAuthenticated(): boolean {
-		return this.state === "active-session" || this.state === "inactive-session"
+		return (
+			this.state === "active-session" || this.state === "attempting-session" || this.state === "inactive-session"
+		)
 	}
 
 	public hasActiveSession(): boolean {
 		return this.state === "active-session"
 	}
 
+	/**
+	 * Check if the user has an active session or is currently attempting to acquire one
+	 *
+	 * @returns True if the user has an active session or is attempting to get one
+	 */
+	public hasOrIsAcquiringActiveSession(): boolean {
+		return this.state === "active-session" || this.state === "attempting-session"
+	}
+
 	/**
 	 * Refresh the session
 	 *
@@ -285,7 +391,6 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 	private async refreshSession(): Promise<void> {
 		if (!this.credentials) {
 			this.log("[auth] Cannot refresh session: missing credentials")
-			this.state = "inactive-session"
 			return
 		}
 
@@ -300,6 +405,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 				this.fetchUserInfo()
 			}
 		} catch (error) {
+			if (error instanceof InvalidClientTokenError) {
+				this.log("[auth] Invalid/Expired client token: clearing credentials")
+				this.clearCredentials()
+			} else if (this.isFirstRefreshAttempt && this.state === "attempting-session") {
+				this.isFirstRefreshAttempt = false
+				this.transitionToInactiveSession()
+			}
 			this.log("[auth] Failed to refresh session", error)
 			throw error
 		}
@@ -323,172 +435,215 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 		return this.userInfo
 	}
 
-	private async clerkSignIn(ticket: string): Promise<{ credentials: AuthCredentials; sessionToken: string }> {
+	/**
+	 * Get the stored organization ID from credentials
+	 *
+	 * @returns The stored organization ID, null for personal accounts or if no credentials exist
+	 */
+	public getStoredOrganizationId(): string | null {
+		return this.credentials?.organizationId || null
+	}
+
+	private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
 		const formData = new URLSearchParams()
 		formData.append("strategy", "ticket")
 		formData.append("ticket", ticket)
 
-		const response = await axios.post(`${getClerkBaseUrl()}/v1/client/sign_ins`, formData, {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sign_ins`, {
+			method: "POST",
 			headers: {
 				"Content-Type": "application/x-www-form-urlencoded",
 				"User-Agent": this.userAgent(),
 			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
 		})
 
-		// 3. Extract the client token from the Authorization header.
-		const clientToken = response.headers.authorization
-
-		if (!clientToken) {
-			throw new Error("No authorization header found in the response")
-		}
-
-		// 4. Find the session using created_session_id and extract the JWT.
-		const sessionId = response.data?.response?.created_session_id
-
-		if (!sessionId) {
-			throw new Error("No session ID found in the response")
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
 		}
 
-		// Find the session in the client sessions array.
-		const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === sessionId)
+		const {
+			response: { created_session_id: sessionId },
+		} = clerkSignInResponseSchema.parse(await response.json())
 
-		if (!session) {
-			throw new Error("Session not found in the response")
-		}
-
-		// Extract the session token (JWT) and store it.
-		const sessionToken = session.last_active_token?.jwt
+		// 3. Extract the client token from the Authorization header.
+		const clientToken = response.headers.get("authorization")
 
-		if (!sessionToken) {
-			throw new Error("Session does not have a token")
+		if (!clientToken) {
+			throw new Error("No authorization header found in the response")
 		}
 
-		const credentials = authCredentialsSchema.parse({ clientToken, sessionId })
-
-		return { credentials, sessionToken }
+		return authCredentialsSchema.parse({ clientToken, sessionId })
 	}
 
 	private async clerkCreateSessionToken(): Promise<string> {
 		const formData = new URLSearchParams()
 		formData.append("_is_native", "1")
 
-		const response = await axios.post(
-			`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`,
-			formData,
-			{
-				headers: {
-					"Content-Type": "application/x-www-form-urlencoded",
-					Authorization: `Bearer ${this.credentials!.clientToken}`,
-					"User-Agent": this.userAgent(),
-				},
-			},
-		)
+		// Handle 3 cases for organization_id:
+		// 1. Have an org id: organization_id=THE_ORG_ID
+		// 2. Have a personal account: organization_id= (empty string)
+		// 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all
+		const organizationId = this.getStoredOrganizationId()
+		if (this.credentials?.organizationId !== undefined) {
+			// We have organization context info (either org id or personal account)
+			formData.append("organization_id", organizationId || "")
+		}
+		// If organizationId is undefined, don't send the param at all (old credentials)
 
-		const sessionToken = response.data?.jwt
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+				Authorization: `Bearer ${this.credentials!.clientToken}`,
+				"User-Agent": this.userAgent(),
+			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
+		})
 
-		if (!sessionToken) {
-			throw new Error("No JWT found in refresh response")
+		if (response.status >= 400 && response.status < 500) {
+			throw new InvalidClientTokenError()
+		} else if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
 		}
 
-		return sessionToken
+		const data = clerkCreateSessionTokenResponseSchema.parse(await response.json())
+
+		return data.jwt
 	}
 
 	private async clerkMe(): Promise<CloudUserInfo> {
-		const response = await axios.get(`${getClerkBaseUrl()}/v1/me`, {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/me`, {
 			headers: {
 				Authorization: `Bearer ${this.credentials!.clientToken}`,
 				"User-Agent": this.userAgent(),
 			},
+			signal: AbortSignal.timeout(10000),
 		})
 
-		const userData = response.data?.response
-
-		if (!userData) {
-			throw new Error("No response user data")
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
 		}
 
+		const { response: userData } = clerkMeResponseSchema.parse(await response.json())
+
 		const userInfo: CloudUserInfo = {}
 
-		userInfo.name = `${userData?.first_name} ${userData?.last_name}`
-		const primaryEmailAddressId = userData?.primary_email_address_id
-		const emailAddresses = userData?.email_addresses
+		userInfo.name = `${userData.first_name} ${userData.last_name}`
+		const primaryEmailAddressId = userData.primary_email_address_id
+		const emailAddresses = userData.email_addresses
 
 		if (primaryEmailAddressId && emailAddresses) {
 			userInfo.email = emailAddresses.find(
-				(email: { id: string }) => primaryEmailAddressId === email?.id,
+				(email: { id: string }) => primaryEmailAddressId === email.id,
 			)?.email_address
 		}
 
-		userInfo.picture = userData?.image_url
+		userInfo.picture = userData.image_url
 
-		// Fetch organization memberships separately
+		// Fetch organization info if user is in organization context
 		try {
-			const orgMemberships = await this.clerkGetOrganizationMemberships()
-			if (orgMemberships && orgMemberships.length > 0) {
-				// Get the first (or active) organization membership
-				const primaryOrgMembership = orgMemberships[0]
-				const organization = primaryOrgMembership?.organization
-
-				if (organization) {
-					userInfo.organizationId = organization.id
-					userInfo.organizationName = organization.name
-					userInfo.organizationRole = primaryOrgMembership.role
+			const storedOrgId = this.getStoredOrganizationId()
+
+			if (this.credentials?.organizationId !== undefined) {
+				// We have organization context info
+				if (storedOrgId !== null) {
+					// User is in organization context - fetch user's memberships and filter
+					const orgMemberships = await this.clerkGetOrganizationMemberships()
+					const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId)
+
+					if (userMembership) {
+						this.setUserOrganizationInfo(userInfo, userMembership)
+						this.log("[auth] User in organization context:", {
+							id: userMembership.organization.id,
+							name: userMembership.organization.name,
+							role: userMembership.role,
+						})
+					} else {
+						this.log("[auth] Warning: User not found in stored organization:", storedOrgId)
+					}
+				} else {
+					this.log("[auth] User in personal account context - not setting organization info")
+				}
+			} else {
+				// Old credentials without organization context - fetch organization info to determine context
+				const orgMemberships = await this.clerkGetOrganizationMemberships()
+				const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships)
+
+				if (primaryOrgMembership) {
+					this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
+					this.log("[auth] Legacy credentials: Found organization membership:", {
+						id: primaryOrgMembership.organization.id,
+						name: primaryOrgMembership.organization.name,
+						role: primaryOrgMembership.role,
+					})
+				} else {
+					this.log("[auth] Legacy credentials: No organization memberships found")
 				}
 			}
 		} catch (error) {
-			this.log("[auth] Failed to fetch organization memberships:", error)
+			this.log("[auth] Failed to fetch organization info:", error)
 			// Don't throw - organization info is optional
 		}
 
 		return userInfo
 	}
 
+	private findOrganizationMembership(
+		memberships: CloudOrganizationMembership[],
+		organizationId: string,
+	): CloudOrganizationMembership | undefined {
+		return memberships?.find((membership) => membership.organization.id === organizationId)
+	}
+
+	private findPrimaryOrganizationMembership(
+		memberships: CloudOrganizationMembership[],
+	): CloudOrganizationMembership | undefined {
+		return memberships && memberships.length > 0 ? memberships[0] : undefined
+	}
+
+	private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void {
+		userInfo.organizationId = membership.organization.id
+		userInfo.organizationName = membership.organization.name
+		userInfo.organizationRole = membership.role
+		userInfo.organizationImageUrl = membership.organization.image_url
+	}
+
 	private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
-		const response = await axios.get(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
 			headers: {
 				Authorization: `Bearer ${this.credentials!.clientToken}`,
 				"User-Agent": this.userAgent(),
 			},
+			signal: AbortSignal.timeout(10000),
 		})
 
-		// The response structure is: { response: [...] }
-		// Extract the organization memberships from the response.response array
-		return response.data?.response || []
+		return clerkOrganizationMembershipsSchema.parse(await response.json()).response
 	}
 
 	private async clerkLogout(credentials: AuthCredentials): Promise<void> {
 		const formData = new URLSearchParams()
 		formData.append("_is_native", "1")
 
-		await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, formData, {
+		const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, {
+			method: "POST",
 			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Bearer ${credentials.clientToken}`,
 				"User-Agent": this.userAgent(),
 			},
+			body: formData.toString(),
+			signal: AbortSignal.timeout(10000),
 		})
-	}
 
-	private userAgent(): string {
-		return getUserAgent(this.context)
-	}
-
-	private static _instance: AuthService | null = null
-
-	static get instance() {
-		if (!this._instance) {
-			throw new Error("AuthService not initialized")
+		if (!response.ok) {
+			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
 		}
-
-		return this._instance
 	}
 
-	static async createInstance(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
-		if (this._instance) {
-			throw new Error("AuthService instance already created")
-		}
-
-		this._instance = new AuthService(context, log)
-		await this._instance.initialize()
-		return this._instance
+	private userAgent(): string {
+		return getUserAgent(this.context)
 	}
 }

+ 31 - 13
packages/cloud/src/CloudService.ts

@@ -37,16 +37,19 @@ export class CloudService {
 		}
 
 		try {
-			this.authService = await AuthService.createInstance(this.context, this.log)
+			this.authService = new AuthService(this.context, this.log)
+			await this.authService.initialize()
 
+			this.authService.on("attempting-session", this.authListener)
 			this.authService.on("inactive-session", this.authListener)
 			this.authService.on("active-session", this.authListener)
 			this.authService.on("logged-out", this.authListener)
 			this.authService.on("user-info", this.authListener)
 
-			this.settingsService = await SettingsService.createInstance(this.context, () =>
+			this.settingsService = new SettingsService(this.context, this.authService, () =>
 				this.callbacks.stateChanged?.(),
 			)
+			this.settingsService.initialize()
 
 			this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
 
@@ -87,6 +90,11 @@ export class CloudService {
 		return this.authService!.hasActiveSession()
 	}
 
+	public hasOrIsAcquiringActiveSession(): boolean {
+		this.ensureInitialized()
+		return this.authService!.hasOrIsAcquiringActiveSession()
+	}
+
 	public getUserInfo(): CloudUserInfo | null {
 		this.ensureInitialized()
 		return this.authService!.getUserInfo()
@@ -110,14 +118,28 @@ export class CloudService {
 		return userInfo?.organizationRole || null
 	}
 
+	public hasStoredOrganizationId(): boolean {
+		this.ensureInitialized()
+		return this.authService!.getStoredOrganizationId() !== null
+	}
+
+	public getStoredOrganizationId(): string | null {
+		this.ensureInitialized()
+		return this.authService!.getStoredOrganizationId()
+	}
+
 	public getAuthState(): string {
 		this.ensureInitialized()
 		return this.authService!.getState()
 	}
 
-	public async handleAuthCallback(code: string | null, state: string | null): Promise<void> {
+	public async handleAuthCallback(
+		code: string | null,
+		state: string | null,
+		organizationId?: string | null,
+	): Promise<void> {
 		this.ensureInitialized()
-		return this.authService!.handleCallback(code, state)
+		return this.authService!.handleCallback(code, state, organizationId)
 	}
 
 	// SettingsService
@@ -136,9 +158,9 @@ export class CloudService {
 
 	// ShareService
 
-	public async shareTask(taskId: string): Promise<boolean> {
+	public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") {
 		this.ensureInitialized()
-		return this.shareService!.shareTask(taskId)
+		return this.shareService!.shareTask(taskId, visibility)
 	}
 
 	public async canShareTask(): Promise<boolean> {
@@ -150,6 +172,8 @@ export class CloudService {
 
 	public dispose(): void {
 		if (this.authService) {
+			this.authService.off("attempting-session", this.authListener)
+			this.authService.off("inactive-session", this.authListener)
 			this.authService.off("active-session", this.authListener)
 			this.authService.off("logged-out", this.authListener)
 			this.authService.off("user-info", this.authListener)
@@ -162,13 +186,7 @@ export class CloudService {
 	}
 
 	private ensureInitialized(): void {
-		if (
-			!this.isInitialized ||
-			!this.authService ||
-			!this.settingsService ||
-			!this.telemetryClient ||
-			!this.shareService
-		) {
+		if (!this.isInitialized) {
 			throw new Error("CloudService not initialized.")
 		}
 	}

+ 9 - 27
packages/cloud/src/SettingsService.ts

@@ -14,21 +14,18 @@ import { RefreshTimer } from "./RefreshTimer"
 const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
 
 export class SettingsService {
-	private static _instance: SettingsService | null = null
-
 	private context: vscode.ExtensionContext
 	private authService: AuthService
 	private settings: OrganizationSettings | undefined = undefined
 	private timer: RefreshTimer
 
-	private constructor(context: vscode.ExtensionContext, authService: AuthService, callback: () => void) {
+	constructor(context: vscode.ExtensionContext, authService: AuthService, callback: () => void) {
 		this.context = context
 		this.authService = authService
 
 		this.timer = new RefreshTimer({
 			callback: async () => {
-				await this.fetchSettings(callback)
-				return true
+				return await this.fetchSettings(callback)
 			},
 			successInterval: 30000,
 			initialBackoffMs: 1000,
@@ -58,11 +55,11 @@ export class SettingsService {
 		}
 	}
 
-	private async fetchSettings(callback: () => void): Promise<void> {
+	private async fetchSettings(callback: () => void): Promise<boolean> {
 		const token = this.authService.getSessionToken()
 
 		if (!token) {
-			return
+			return false
 		}
 
 		try {
@@ -74,7 +71,7 @@ export class SettingsService {
 
 			if (!response.ok) {
 				console.error(`Failed to fetch organization settings: ${response.status} ${response.statusText}`)
-				return
+				return false
 			}
 
 			const data = await response.json()
@@ -82,7 +79,7 @@ export class SettingsService {
 
 			if (!result.success) {
 				console.error("Invalid organization settings format:", result.error)
-				return
+				return false
 			}
 
 			const newSettings = result.data
@@ -92,8 +89,11 @@ export class SettingsService {
 				await this.cacheSettings()
 				callback()
 			}
+
+			return true
 		} catch (error) {
 			console.error("Error fetching organization settings:", error)
+			return false
 		}
 	}
 
@@ -121,22 +121,4 @@ export class SettingsService {
 	public dispose(): void {
 		this.timer.stop()
 	}
-
-	static get instance() {
-		if (!this._instance) {
-			throw new Error("SettingsService not initialized")
-		}
-
-		return this._instance
-	}
-
-	static async createInstance(context: vscode.ExtensionContext, callback: () => void) {
-		if (this._instance) {
-			throw new Error("SettingsService instance already created")
-		}
-
-		this._instance = new SettingsService(context, AuthService.instance, callback)
-		this._instance.initialize()
-		return this._instance
-	}
 }

+ 10 - 10
packages/cloud/src/ShareService.ts

@@ -7,6 +7,8 @@ import type { AuthService } from "./AuthService"
 import type { SettingsService } from "./SettingsService"
 import { getUserAgent } from "./utils"
 
+export type ShareVisibility = "organization" | "public"
+
 export class ShareService {
 	private authService: AuthService
 	private settingsService: SettingsService
@@ -19,19 +21,19 @@ export class ShareService {
 	}
 
 	/**
-	 * Share a task: Create link and copy to clipboard
-	 * Returns true if successful, false if failed
+	 * Share a task with specified visibility
+	 * Returns the share response data
 	 */
-	async shareTask(taskId: string): Promise<boolean> {
+	async shareTask(taskId: string, visibility: ShareVisibility = "organization") {
 		try {
 			const sessionToken = this.authService.getSessionToken()
 			if (!sessionToken) {
-				return false
+				throw new Error("Authentication required")
 			}
 
 			const response = await axios.post(
 				`${getRooCodeApiUrl()}/api/extension/share`,
-				{ taskId },
+				{ taskId, visibility },
 				{
 					headers: {
 						"Content-Type": "application/json",
@@ -47,14 +49,12 @@ export class ShareService {
 			if (data.success && data.shareUrl) {
 				// Copy to clipboard
 				await vscode.env.clipboard.writeText(data.shareUrl)
-				return true
-			} else {
-				this.log("[share] Share failed:", data.error)
-				return false
 			}
+
+			return data
 		} catch (error) {
 			this.log("[share] Error sharing task:", error)
-			return false
+			throw error
 		}
 	}
 

+ 8 - 0
packages/cloud/src/__mocks__/vscode.ts

@@ -18,14 +18,18 @@ export interface ExtensionContext {
 		get: (key: string) => Promise<string | undefined>
 		store: (key: string, value: string) => Promise<void>
 		delete: (key: string) => Promise<void>
+		onDidChange: (listener: (e: { key: string }) => void) => { dispose: () => void }
 	}
 	globalState: {
 		get: <T>(key: string) => T | undefined
 		update: (key: string, value: any) => Promise<void>
 	}
+	subscriptions: any[]
 	extension?: {
 		packageJSON?: {
 			version?: string
+			publisher?: string
+			name?: string
 		}
 	}
 }
@@ -36,14 +40,18 @@ export const mockExtensionContext: ExtensionContext = {
 		get: vi.fn().mockResolvedValue(undefined),
 		store: vi.fn().mockResolvedValue(undefined),
 		delete: vi.fn().mockResolvedValue(undefined),
+		onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
 	},
 	globalState: {
 		get: vi.fn().mockReturnValue(undefined),
 		update: vi.fn().mockResolvedValue(undefined),
 	},
+	subscriptions: [],
 	extension: {
 		packageJSON: {
 			version: "1.0.0",
+			publisher: "RooVeterinaryInc",
+			name: "roo-cline",
 		},
 	},
 }

+ 1094 - 0
packages/cloud/src/__tests__/AuthService.spec.ts

@@ -0,0 +1,1094 @@
+// npx vitest run src/__tests__/AuthService.spec.ts
+
+import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest"
+import crypto from "crypto"
+import * as vscode from "vscode"
+
+import { AuthService } from "../AuthService"
+import { RefreshTimer } from "../RefreshTimer"
+import * as Config from "../Config"
+import * as utils from "../utils"
+
+// Mock external dependencies
+vi.mock("../RefreshTimer")
+vi.mock("../Config")
+vi.mock("../utils")
+vi.mock("crypto")
+
+// Mock fetch globally
+const mockFetch = vi.fn()
+global.fetch = mockFetch
+
+// Mock vscode module
+vi.mock("vscode", () => ({
+	window: {
+		showInformationMessage: vi.fn(),
+		showErrorMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+		uriScheme: "vscode",
+	},
+	Uri: {
+		parse: vi.fn((uri: string) => ({ toString: () => uri })),
+	},
+}))
+
+describe("AuthService", () => {
+	let authService: AuthService
+	let mockTimer: {
+		start: Mock
+		stop: Mock
+		reset: Mock
+	}
+	let mockLog: Mock
+	let mockContext: {
+		subscriptions: { push: Mock }
+		secrets: {
+			get: Mock
+			store: Mock
+			delete: Mock
+			onDidChange: Mock
+		}
+		globalState: {
+			get: Mock
+			update: Mock
+		}
+		extension: {
+			packageJSON: {
+				version: string
+				publisher: string
+				name: string
+			}
+		}
+	}
+
+	beforeEach(() => {
+		// Reset all mocks
+		vi.clearAllMocks()
+
+		// Setup mock context with proper subscriptions array
+		mockContext = {
+			subscriptions: {
+				push: vi.fn(),
+			},
+			secrets: {
+				get: vi.fn().mockResolvedValue(undefined),
+				store: vi.fn().mockResolvedValue(undefined),
+				delete: vi.fn().mockResolvedValue(undefined),
+				onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			},
+			globalState: {
+				get: vi.fn().mockReturnValue(undefined),
+				update: vi.fn().mockResolvedValue(undefined),
+			},
+			extension: {
+				packageJSON: {
+					version: "1.0.0",
+					publisher: "RooVeterinaryInc",
+					name: "roo-cline",
+				},
+			},
+		}
+
+		// Setup timer mock
+		mockTimer = {
+			start: vi.fn(),
+			stop: vi.fn(),
+			reset: vi.fn(),
+		}
+		vi.mocked(RefreshTimer).mockImplementation(() => mockTimer as unknown as RefreshTimer)
+
+		// Setup config mocks - use production URL by default to maintain existing test behavior
+		vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
+		vi.mocked(Config.getRooCodeApiUrl).mockReturnValue("https://api.test.com")
+
+		// Setup utils mock
+		vi.mocked(utils.getUserAgent).mockReturnValue("Roo-Code 1.0.0")
+
+		// Setup crypto mock
+		vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never)
+
+		// Setup log mock
+		mockLog = vi.fn()
+
+		authService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("constructor", () => {
+		it("should initialize with correct default values", () => {
+			expect(authService.getState()).toBe("initializing")
+			expect(authService.isAuthenticated()).toBe(false)
+			expect(authService.hasActiveSession()).toBe(false)
+			expect(authService.getSessionToken()).toBeUndefined()
+			expect(authService.getUserInfo()).toBeNull()
+		})
+
+		it("should create RefreshTimer with correct configuration", () => {
+			expect(RefreshTimer).toHaveBeenCalledWith({
+				callback: expect.any(Function),
+				successInterval: 50_000,
+				initialBackoffMs: 1_000,
+				maxBackoffMs: 300_000,
+			})
+		})
+
+		it("should use console.log as default logger", () => {
+			const serviceWithoutLog = new AuthService(mockContext as unknown as vscode.ExtensionContext)
+			// Can't directly test console.log usage, but constructor should not throw
+			expect(serviceWithoutLog).toBeInstanceOf(AuthService)
+		})
+	})
+
+	describe("initialize", () => {
+		it("should handle credentials change and setup event listener", async () => {
+			await authService.initialize()
+
+			expect(mockContext.subscriptions.push).toHaveBeenCalled()
+			expect(mockContext.secrets.onDidChange).toHaveBeenCalled()
+		})
+
+		it("should not initialize twice", async () => {
+			await authService.initialize()
+			const firstCallCount = vi.mocked(mockContext.secrets.onDidChange).mock.calls.length
+
+			await authService.initialize()
+			expect(mockContext.secrets.onDidChange).toHaveBeenCalledTimes(firstCallCount)
+			expect(mockLog).toHaveBeenCalledWith("[auth] initialize() called after already initialized")
+		})
+
+		it("should transition to logged-out when no credentials exist", async () => {
+			mockContext.secrets.get.mockResolvedValue(undefined)
+
+			const loggedOutSpy = vi.fn()
+			authService.on("logged-out", loggedOutSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "initializing" })
+		})
+
+		it("should transition to attempting-session when valid credentials exist", async () => {
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const attemptingSessionSpy = vi.fn()
+			authService.on("attempting-session", attemptingSessionSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("attempting-session")
+			expect(attemptingSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" })
+			expect(mockTimer.start).toHaveBeenCalled()
+		})
+
+		it("should handle invalid credentials gracefully", async () => {
+			mockContext.secrets.get.mockResolvedValue("invalid-json")
+
+			const loggedOutSpy = vi.fn()
+			authService.on("logged-out", loggedOutSpy)
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error))
+		})
+
+		it("should handle credentials change events", async () => {
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			await authService.initialize()
+
+			// Simulate credentials change event
+			const newCredentials = { clientToken: "new-token", sessionId: "new-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials))
+
+			const attemptingSessionSpy = vi.fn()
+			authService.on("attempting-session", attemptingSessionSpy)
+
+			onDidChangeCallback!({ key: "clerk-auth-credentials" })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(attemptingSessionSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("login", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should generate state and open external URL", async () => {
+			const mockOpenExternal = vi.fn()
+			const vscode = await import("vscode")
+			vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal)
+
+			await authService.login()
+
+			expect(crypto.randomBytes).toHaveBeenCalledWith(16)
+			expect(mockContext.globalState.update).toHaveBeenCalledWith(
+				"clerk-auth-state",
+				"746573742d72616e646f6d2d6279746573",
+			)
+			expect(mockOpenExternal).toHaveBeenCalledWith(
+				expect.objectContaining({
+					toString: expect.any(Function),
+				}),
+			)
+		})
+
+		it("should use package.json values for redirect URI", async () => {
+			const mockOpenExternal = vi.fn()
+			const vscode = await import("vscode")
+			vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal)
+
+			await authService.login()
+
+			const expectedUrl =
+				"https://api.test.com/extension/sign-in?state=746573742d72616e646f6d2d6279746573&auth_redirect=vscode%3A%2F%2FRooVeterinaryInc.roo-cline"
+			expect(mockOpenExternal).toHaveBeenCalledWith(
+				expect.objectContaining({
+					toString: expect.any(Function),
+				}),
+			)
+
+			// Verify the actual URL
+			const calledUri = mockOpenExternal.mock.calls[0][0]
+			expect(calledUri.toString()).toBe(expectedUrl)
+		})
+
+		it("should handle errors during login", async () => {
+			vi.mocked(crypto.randomBytes).mockImplementation(() => {
+				throw new Error("Crypto error")
+			})
+
+			await expect(authService.login()).rejects.toThrow("Failed to initiate Roo Code Cloud authentication")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error initiating Roo Code Cloud auth: Error: Crypto error")
+		})
+	})
+
+	describe("handleCallback", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should handle invalid parameters", async () => {
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.handleCallback(null, "state")
+			expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url")
+
+			await authService.handleCallback("code", null)
+			expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url")
+		})
+
+		it("should validate state parameter", async () => {
+			mockContext.globalState.get.mockReturnValue("stored-state")
+
+			await expect(authService.handleCallback("code", "different-state")).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+			expect(mockLog).toHaveBeenCalledWith("[auth] State mismatch in callback")
+		})
+
+		it("should successfully handle valid callback", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			// Mock successful Clerk sign-in response
+			const mockResponse = {
+				ok: true,
+				json: () =>
+					Promise.resolve({
+						response: { created_session_id: "session-123" },
+					}),
+				headers: {
+					get: (header: string) => (header === "authorization" ? "Bearer token-123" : null),
+				},
+			}
+			mockFetch.mockResolvedValue(mockResponse)
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.handleCallback("auth-code", storedState)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				"clerk-auth-credentials",
+				JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123", organizationId: null }),
+			)
+			expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud")
+		})
+
+		it("should handle Clerk API errors", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 400,
+				statusText: "Bad Request",
+			})
+
+			const loggedOutSpy = vi.fn()
+			authService.on("logged-out", loggedOutSpy)
+
+			await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+			expect(loggedOutSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("logout", () => {
+		beforeEach(async () => {
+			await authService.initialize()
+		})
+
+		it("should clear credentials and call Clerk logout", async () => {
+			// Set up credentials first by simulating a login state
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+
+			// Manually set the credentials in the service
+			authService["credentials"] = credentials
+
+			// Mock successful logout response
+			mockFetch.mockResolvedValue({ ok: true })
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("clerk-auth-state", undefined)
+			expect(mockFetch).toHaveBeenCalledWith(
+				"https://clerk.roocode.com/v1/client/sessions/test-session/remove",
+				expect.objectContaining({
+					method: "POST",
+					headers: expect.objectContaining({
+						Authorization: "Bearer test-token",
+					}),
+				}),
+			)
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+
+		it("should handle logout without credentials", async () => {
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalled()
+			expect(mockFetch).not.toHaveBeenCalled()
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+
+		it("should handle Clerk logout errors gracefully", async () => {
+			// Set up credentials first by simulating a login state
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+
+			// Manually set the credentials in the service
+			authService["credentials"] = credentials
+
+			// Mock failed logout response
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			const vscode = await import("vscode")
+			const mockShowInfo = vi.fn()
+			vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
+
+			await authService.logout()
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error calling clerkLogout:", expect.any(Error))
+			expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud")
+		})
+	})
+
+	describe("state management", () => {
+		it("should return correct state", () => {
+			expect(authService.getState()).toBe("initializing")
+		})
+
+		it("should return correct authentication status", async () => {
+			await authService.initialize()
+			expect(authService.isAuthenticated()).toBe(false)
+
+			// Create a new service instance with credentials
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const authenticatedService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			await authenticatedService.initialize()
+
+			expect(authenticatedService.isAuthenticated()).toBe(true)
+			expect(authenticatedService.hasActiveSession()).toBe(false)
+		})
+
+		it("should return session token only for active sessions", () => {
+			expect(authService.getSessionToken()).toBeUndefined()
+
+			// Manually set state to active-session for testing
+			// This would normally happen through refreshSession
+			authService["state"] = "active-session"
+			authService["sessionToken"] = "test-jwt"
+
+			expect(authService.getSessionToken()).toBe("test-jwt")
+		})
+
+		it("should return correct values for new methods", async () => {
+			await authService.initialize()
+			expect(authService.hasOrIsAcquiringActiveSession()).toBe(false)
+
+			// Create a new service instance with credentials (attempting-session)
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const attemptingService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			await attemptingService.initialize()
+
+			expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
+			expect(attemptingService.hasActiveSession()).toBe(false)
+
+			// Manually set state to active-session for testing
+			attemptingService["state"] = "active-session"
+			expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
+			expect(attemptingService.hasActiveSession()).toBe(true)
+		})
+	})
+
+	describe("session refresh", () => {
+		beforeEach(async () => {
+			// Set up with credentials
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+		})
+
+		it("should refresh session successfully", async () => {
+			// Mock successful token creation and user info fetch
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "new-jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "John",
+								last_name: "Doe",
+								image_url: "https://example.com/avatar.jpg",
+								primary_email_address_id: "email-1",
+								email_addresses: [{ id: "email-1", email_address: "[email protected]" }],
+							},
+						}),
+				})
+
+			const activeSessionSpy = vi.fn()
+			const userInfoSpy = vi.fn()
+			authService.on("active-session", activeSessionSpy)
+			authService.on("user-info", userInfoSpy)
+
+			// Trigger refresh by calling the timer callback
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(authService.getState()).toBe("active-session")
+			expect(authService.hasActiveSession()).toBe(true)
+			expect(authService.getSessionToken()).toBe("new-jwt-token")
+			expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" })
+			expect(userInfoSpy).toHaveBeenCalledWith({
+				userInfo: {
+					name: "John Doe",
+					email: "[email protected]",
+					picture: "https://example.com/avatar.jpg",
+				},
+			})
+		})
+
+		it("should handle invalid client token error", async () => {
+			// Mock 401 response (invalid token)
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 401,
+				statusText: "Unauthorized",
+			})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+
+			await expect(timerCallback()).rejects.toThrow()
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials")
+		})
+
+		it("should handle network errors during refresh", async () => {
+			mockFetch.mockRejectedValue(new Error("Network error"))
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+
+			await expect(timerCallback()).rejects.toThrow("Network error")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to refresh session", expect.any(Error))
+		})
+
+		it("should transition to inactive-session on first attempt failure", async () => {
+			// Mock failed token creation response
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+			})
+
+			const inactiveSessionSpy = vi.fn()
+			authService.on("inactive-session", inactiveSessionSpy)
+
+			// Verify we start in attempting-session state
+			expect(authService.getState()).toBe("attempting-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(true)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+
+			await expect(timerCallback()).rejects.toThrow()
+
+			// Should transition to inactive-session after first failure
+			expect(authService.getState()).toBe("inactive-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(false)
+			expect(inactiveSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" })
+		})
+
+		it("should not transition to inactive-session on subsequent failures", async () => {
+			// First, transition to inactive-session by failing the first attempt
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+			})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await expect(timerCallback()).rejects.toThrow()
+
+			// Verify we're now in inactive-session
+			expect(authService.getState()).toBe("inactive-session")
+			expect(authService["isFirstRefreshAttempt"]).toBe(false)
+
+			const inactiveSessionSpy = vi.fn()
+			authService.on("inactive-session", inactiveSessionSpy)
+
+			// Subsequent failure should not trigger another transition
+			await expect(timerCallback()).rejects.toThrow()
+
+			expect(authService.getState()).toBe("inactive-session")
+			expect(inactiveSessionSpy).not.toHaveBeenCalled()
+		})
+
+		it("should clear credentials on 401 during first refresh attempt (bug fix)", async () => {
+			// Mock 401 response during first refresh attempt
+			mockFetch.mockResolvedValue({
+				ok: false,
+				status: 401,
+				statusText: "Unauthorized",
+			})
+
+			const loggedOutSpy = vi.fn()
+			authService.on("logged-out", loggedOutSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await expect(timerCallback()).rejects.toThrow()
+
+			// Should clear credentials (not just transition to inactive-session)
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials")
+
+			// Simulate credentials cleared event
+			mockContext.secrets.get.mockResolvedValue(undefined)
+			await authService["handleCredentialsChange"]()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "attempting-session" })
+		})
+	})
+
+	describe("user info", () => {
+		it("should return null initially", () => {
+			expect(authService.getUserInfo()).toBeNull()
+		})
+
+		it("should parse user info correctly for personal accounts", async () => {
+			// Set up with credentials for personal account (no organizationId)
+			const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock successful responses
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Jane",
+								last_name: "Smith",
+								image_url: "https://example.com/jane.jpg",
+								primary_email_address_id: "email-2",
+								email_addresses: [
+									{ id: "email-1", email_address: "[email protected]" },
+									{ id: "email-2", email_address: "[email protected]" },
+								],
+							},
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				name: "Jane Smith",
+				email: "[email protected]",
+				picture: "https://example.com/jane.jpg",
+			})
+		})
+
+		it("should parse user info correctly for organization accounts", async () => {
+			// Set up with credentials for organization account
+			const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: "org_1" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock successful responses
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Jane",
+								last_name: "Smith",
+								image_url: "https://example.com/jane.jpg",
+								primary_email_address_id: "email-2",
+								email_addresses: [
+									{ id: "email-1", email_address: "[email protected]" },
+									{ id: "email-2", email_address: "[email protected]" },
+								],
+							},
+						}),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: [
+								{
+									id: "org_member_id_1",
+									role: "member",
+									organization: {
+										id: "org_1",
+										name: "Org 1",
+									},
+								},
+							],
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				name: "Jane Smith",
+				email: "[email protected]",
+				picture: "https://example.com/jane.jpg",
+				organizationId: "org_1",
+				organizationName: "Org 1",
+				organizationRole: "member",
+			})
+		})
+
+		it("should handle missing user info fields", async () => {
+			// Set up with credentials for personal account (no organizationId)
+			const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock responses with minimal data
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "John",
+								last_name: "Doe",
+								// Missing other fields
+							},
+						}),
+				})
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			const userInfo = authService.getUserInfo()
+			expect(userInfo).toEqual({
+				name: "John Doe",
+				email: undefined,
+				picture: undefined,
+			})
+		})
+	})
+
+	describe("event emissions", () => {
+		it("should emit logged-out event", async () => {
+			const loggedOutSpy = vi.fn()
+			authService.on("logged-out", loggedOutSpy)
+
+			await authService.initialize()
+
+			expect(loggedOutSpy).toHaveBeenCalledWith({ previousState: "initializing" })
+		})
+
+		it("should emit attempting-session event", async () => {
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			const attemptingSessionSpy = vi.fn()
+			authService.on("attempting-session", attemptingSessionSpy)
+
+			await authService.initialize()
+
+			expect(attemptingSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" })
+		})
+
+		it("should emit active-session event", async () => {
+			// Set up with credentials
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			// Mock both the token creation and user info fetch
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Test",
+								last_name: "User",
+							},
+						}),
+				})
+
+			const activeSessionSpy = vi.fn()
+			authService.on("active-session", activeSessionSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "attempting-session" })
+		})
+
+		it("should emit user-info event", async () => {
+			// Set up with credentials
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+			await authService.initialize()
+
+			// Clear previous mock calls
+			mockFetch.mockClear()
+
+			mockFetch
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () => Promise.resolve({ jwt: "jwt-token" }),
+				})
+				.mockResolvedValueOnce({
+					ok: true,
+					json: () =>
+						Promise.resolve({
+							response: {
+								first_name: "Test",
+								last_name: "User",
+							},
+						}),
+				})
+
+			const userInfoSpy = vi.fn()
+			authService.on("user-info", userInfoSpy)
+
+			const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
+			await timerCallback()
+
+			// Wait for async operations to complete
+			await new Promise((resolve) => setTimeout(resolve, 0))
+
+			expect(userInfoSpy).toHaveBeenCalledWith({
+				userInfo: {
+					name: "Test User",
+					email: undefined,
+					picture: undefined,
+				},
+			})
+		})
+	})
+
+	describe("error handling", () => {
+		it("should handle credentials change errors", async () => {
+			mockContext.secrets.get.mockRejectedValue(new Error("Storage error"))
+
+			await authService.initialize()
+
+			expect(mockLog).toHaveBeenCalledWith("[auth] Error handling credentials change:", expect.any(Error))
+		})
+
+		it("should handle malformed JSON in credentials", async () => {
+			mockContext.secrets.get.mockResolvedValue("invalid-json{")
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error))
+		})
+
+		it("should handle invalid credentials schema", async () => {
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify({ invalid: "data" }))
+
+			await authService.initialize()
+
+			expect(authService.getState()).toBe("logged-out")
+			expect(mockLog).toHaveBeenCalledWith("[auth] Invalid credentials format:", expect.any(Array))
+		})
+
+		it("should handle missing authorization header in sign-in response", async () => {
+			const storedState = "valid-state"
+			mockContext.globalState.get.mockReturnValue(storedState)
+
+			mockFetch.mockResolvedValue({
+				ok: true,
+				json: () =>
+					Promise.resolve({
+						response: { created_session_id: "session-123" },
+					}),
+				headers: {
+					get: () => null, // No authorization header
+				},
+			})
+
+			await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow(
+				"Failed to handle Roo Code Cloud callback",
+			)
+		})
+	})
+
+	describe("timer integration", () => {
+		it("should stop timer on logged-out transition", async () => {
+			await authService.initialize()
+
+			expect(mockTimer.stop).toHaveBeenCalled()
+		})
+
+		it("should start timer on attempting-session transition", async () => {
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			await authService.initialize()
+
+			expect(mockTimer.start).toHaveBeenCalled()
+		})
+	})
+
+	describe("auth credentials key scoping", () => {
+		it("should use default key when getClerkBaseUrl returns production URL", async () => {
+			// Mock getClerkBaseUrl to return production URL
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+
+			await service.initialize()
+			await service["storeCredentials"](credentials)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				"clerk-auth-credentials",
+				JSON.stringify(credentials),
+			)
+		})
+
+		it("should use scoped key when getClerkBaseUrl returns custom URL", async () => {
+			const customUrl = "https://custom.clerk.com"
+			// Mock getClerkBaseUrl to return custom URL
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+
+			await service.initialize()
+			await service["storeCredentials"](credentials)
+
+			expect(mockContext.secrets.store).toHaveBeenCalledWith(
+				`clerk-auth-credentials-${customUrl}`,
+				JSON.stringify(credentials),
+			)
+		})
+
+		it("should load credentials using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const credentials = { clientToken: "test-token", sessionId: "test-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
+
+			await service.initialize()
+			const loadedCredentials = await service["loadCredentials"]()
+
+			expect(mockContext.secrets.get).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
+			expect(loadedCredentials).toEqual(credentials)
+		})
+
+		it("should clear credentials using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+
+			await service.initialize()
+			await service["clearCredentials"]()
+
+			expect(mockContext.secrets.delete).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
+		})
+
+		it("should listen for changes on scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			await service.initialize()
+
+			// Simulate credentials change event with scoped key
+			const newCredentials = { clientToken: "new-token", sessionId: "new-session" }
+			mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials))
+
+			const attemptingSessionSpy = vi.fn()
+			service.on("attempting-session", attemptingSessionSpy)
+
+			onDidChangeCallback!({ key: `clerk-auth-credentials-${customUrl}` })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(attemptingSessionSpy).toHaveBeenCalled()
+		})
+
+		it("should not respond to changes on different scoped keys", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			await service.initialize()
+
+			const inactiveSessionSpy = vi.fn()
+			service.on("inactive-session", inactiveSessionSpy)
+
+			// Simulate credentials change event with different scoped key
+			onDidChangeCallback!({ key: "clerk-auth-credentials-https://other.clerk.com" })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(inactiveSessionSpy).not.toHaveBeenCalled()
+		})
+
+		it("should not respond to changes on default key when using scoped key", async () => {
+			const customUrl = "https://custom.clerk.com"
+			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+
+			let onDidChangeCallback: (e: { key: string }) => void
+
+			mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
+				onDidChangeCallback = callback
+				return { dispose: vi.fn() }
+			})
+
+			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			await service.initialize()
+
+			const inactiveSessionSpy = vi.fn()
+			service.on("inactive-session", inactiveSessionSpy)
+
+			// Simulate credentials change event with default key
+			onDidChangeCallback!({ key: "clerk-auth-credentials" })
+			await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
+
+			expect(inactiveSessionSpy).not.toHaveBeenCalled()
+		})
+	})
+})

+ 42 - 9
packages/cloud/src/__tests__/CloudService.test.ts

@@ -40,6 +40,7 @@ describe("CloudService", () => {
 		getState: ReturnType<typeof vi.fn>
 		getSessionToken: ReturnType<typeof vi.fn>
 		handleCallback: ReturnType<typeof vi.fn>
+		getStoredOrganizationId: ReturnType<typeof vi.fn>
 		on: ReturnType<typeof vi.fn>
 		off: ReturnType<typeof vi.fn>
 		once: ReturnType<typeof vi.fn>
@@ -79,7 +80,7 @@ describe("CloudService", () => {
 		} as unknown as vscode.ExtensionContext
 
 		mockAuthService = {
-			initialize: vi.fn(),
+			initialize: vi.fn().mockResolvedValue(undefined),
 			login: vi.fn(),
 			logout: vi.fn(),
 			isAuthenticated: vi.fn().mockReturnValue(false),
@@ -88,6 +89,7 @@ describe("CloudService", () => {
 			getState: vi.fn().mockReturnValue("logged-out"),
 			getSessionToken: vi.fn(),
 			handleCallback: vi.fn(),
+			getStoredOrganizationId: vi.fn().mockReturnValue(null),
 			on: vi.fn(),
 			off: vi.fn(),
 			once: vi.fn(),
@@ -108,11 +110,8 @@ describe("CloudService", () => {
 			},
 		}
 
-		vi.mocked(AuthService.createInstance).mockResolvedValue(mockAuthService as unknown as AuthService)
-		Object.defineProperty(AuthService, "instance", { get: () => mockAuthService, configurable: true })
-
-		vi.mocked(SettingsService.createInstance).mockResolvedValue(mockSettingsService as unknown as SettingsService)
-		Object.defineProperty(SettingsService, "instance", { get: () => mockSettingsService, configurable: true })
+		vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService)
+		vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService)
 
 		vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
 		Object.defineProperty(TelemetryService, "instance", {
@@ -135,8 +134,8 @@ describe("CloudService", () => {
 			const cloudService = await CloudService.createInstance(mockContext, callbacks)
 
 			expect(cloudService).toBeInstanceOf(CloudService)
-			expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
-			expect(SettingsService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
+			expect(AuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
+			expect(SettingsService).toHaveBeenCalledWith(mockContext, mockAuthService, expect.any(Function))
 		})
 
 		it("should throw error if instance already exists", async () => {
@@ -258,7 +257,41 @@ describe("CloudService", () => {
 
 		it("should delegate handleAuthCallback to AuthService", async () => {
 			await cloudService.handleAuthCallback("code", "state")
-			expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state")
+			expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined)
+		})
+
+		it("should delegate handleAuthCallback with organizationId to AuthService", async () => {
+			await cloudService.handleAuthCallback("code", "state", "org_123")
+			expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123")
+		})
+
+		it("should return stored organization ID from AuthService", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue("org_456")
+
+			const result = cloudService.getStoredOrganizationId()
+			expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled()
+			expect(result).toBe("org_456")
+		})
+
+		it("should return null when no stored organization ID available", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue(null)
+
+			const result = cloudService.getStoredOrganizationId()
+			expect(result).toBe(null)
+		})
+
+		it("should return true when stored organization ID exists", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue("org_789")
+
+			const result = cloudService.hasStoredOrganizationId()
+			expect(result).toBe(true)
+		})
+
+		it("should return false when no stored organization ID exists", () => {
+			mockAuthService.getStoredOrganizationId.mockReturnValue(null)
+
+			const result = cloudService.hasStoredOrganizationId()
+			expect(result).toBe(false)
 		})
 	})
 

+ 49 - 35
packages/cloud/src/__tests__/ShareService.test.ts

@@ -17,6 +17,7 @@ vi.mock("vscode", () => ({
 	window: {
 		showInformationMessage: vi.fn(),
 		showErrorMessage: vi.fn(),
+		showQuickPick: vi.fn(),
 	},
 	env: {
 		clipboard: {
@@ -68,7 +69,7 @@ describe("ShareService", () => {
 	})
 
 	describe("shareTask", () => {
-		it("should share task and copy to clipboard", async () => {
+		it("should share task with organization visibility and copy to clipboard", async () => {
 			const mockResponse = {
 				data: {
 					success: true,
@@ -76,16 +77,16 @@ describe("ShareService", () => {
 				},
 			}
 
-			;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
 			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
 			mockedAxios.post.mockResolvedValue(mockResponse)
 
-			const result = await shareService.shareTask("task-123")
+			const result = await shareService.shareTask("task-123", "organization")
 
-			expect(result).toBe(true)
+			expect(result.success).toBe(true)
+			expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123")
 			expect(mockedAxios.post).toHaveBeenCalledWith(
 				"https://app.roocode.com/api/extension/share",
-				{ taskId: "task-123" },
+				{ taskId: "task-123", visibility: "organization" },
 				{
 					headers: {
 						"Content-Type": "application/json",
@@ -97,63 +98,76 @@ describe("ShareService", () => {
 			expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123")
 		})
 
-		it("should handle API error response", async () => {
+		it("should share task with public visibility", async () => {
 			const mockResponse = {
 				data: {
-					success: false,
-					error: "Task not found",
+					success: true,
+					shareUrl: "https://app.roocode.com/share/abc123",
 				},
 			}
 
-			;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
 			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
 			mockedAxios.post.mockResolvedValue(mockResponse)
 
-			const result = await shareService.shareTask("task-123")
+			const result = await shareService.shareTask("task-123", "public")
 
-			expect(result).toBe(false)
+			expect(result.success).toBe(true)
+			expect(mockedAxios.post).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/extension/share",
+				{ taskId: "task-123", visibility: "public" },
+				expect.any(Object),
+			)
 		})
 
-		it("should handle authentication errors", async () => {
-			;(mockAuthService.hasActiveSession as any).mockReturnValue(false)
+		it("should default to organization visibility when not specified", async () => {
+			const mockResponse = {
+				data: {
+					success: true,
+					shareUrl: "https://app.roocode.com/share/abc123",
+				},
+			}
+
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockedAxios.post.mockResolvedValue(mockResponse)
 
 			const result = await shareService.shareTask("task-123")
 
-			expect(result).toBe(false)
-			expect(mockedAxios.post).not.toHaveBeenCalled()
+			expect(result.success).toBe(true)
+			expect(mockedAxios.post).toHaveBeenCalledWith(
+				"https://app.roocode.com/api/extension/share",
+				{ taskId: "task-123", visibility: "organization" },
+				expect.any(Object),
+			)
 		})
 
-		it("should handle 403 error for disabled sharing", async () => {
-			;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
-			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
-
-			const error = {
-				isAxiosError: true,
-				response: {
-					status: 403,
-					data: {
-						error: "Task sharing is not enabled for this organization",
-					},
+		it("should handle API error response", async () => {
+			const mockResponse = {
+				data: {
+					success: false,
+					error: "Task not found",
 				},
 			}
 
-			mockedAxios.isAxiosError.mockReturnValue(true)
-			mockedAxios.post.mockRejectedValue(error)
+			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
+			mockedAxios.post.mockResolvedValue(mockResponse)
 
-			const result = await shareService.shareTask("task-123")
+			const result = await shareService.shareTask("task-123", "organization")
 
-			expect(result).toBe(false)
+			expect(result.success).toBe(false)
+			expect(result.error).toBe("Task not found")
+		})
+
+		it("should handle authentication errors", async () => {
+			;(mockAuthService.getSessionToken as any).mockReturnValue(null)
+
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required")
 		})
 
 		it("should handle unexpected errors", async () => {
-			;(mockAuthService.hasActiveSession as any).mockReturnValue(true)
 			;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
-
 			mockedAxios.post.mockRejectedValue(new Error("Network error"))
 
-			const result = await shareService.shareTask("task-123")
-
-			expect(result).toBe(false)
+			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error")
 		})
 	})
 

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

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

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

@@ -13,6 +13,7 @@ export interface CloudUserInfo {
 	organizationId?: string
 	organizationName?: string
 	organizationRole?: string
+	organizationImageUrl?: string
 }
 
 /**
@@ -124,6 +125,7 @@ export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = {
 export const ORGANIZATION_DEFAULT: OrganizationSettings = {
 	version: 0,
 	cloudSettings: {
+		recordTaskMessages: true,
 		enableTaskSharing: true,
 		taskShareExpirationDays: 30,
 	},
@@ -139,6 +141,8 @@ export const shareResponseSchema = z.object({
 	success: z.boolean(),
 	shareUrl: z.string().optional(),
 	error: z.string().optional(),
+	isNewShare: z.boolean().optional(),
+	manageUrl: z.string().optional(),
 })
 
 export type ShareResponse = z.infer<typeof shareResponseSchema>

+ 1 - 2
packages/types/src/experiment.ts

@@ -7,7 +7,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
  */
 
 const kilocodeExperimentIds = ["autocomplete"] as const
-export const experimentIds = ["powerSteering", "disableCompletionCommand", "multiFileApplyDiff"] as const
+export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const
 
 export const experimentIdsSchema = z.enum([...experimentIds, ...kilocodeExperimentIds])
 
@@ -19,7 +19,6 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
 
 export const experimentsSchema = z.object({
 	powerSteering: z.boolean().optional(),
-	disableCompletionCommand: z.boolean().optional(),
 	multiFileApplyDiff: z.boolean().optional(),
 	autocomplete: z.boolean(), // kilocode_change
 })

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

@@ -111,6 +111,7 @@ export const globalSettingsSchema = z.object({
 	enhancementApiConfigId: z.string().optional(),
 	commitMessageApiConfigId: z.string().optional(), // kilocode_change
 	historyPreviewCollapsed: z.boolean().optional(),
+	profileThresholds: z.record(z.string(), z.number()).optional(),
 })
 
 export type GlobalSettings = z.infer<typeof globalSettingsSchema>

+ 7 - 0
packages/types/src/provider-settings.ts

@@ -9,6 +9,7 @@ import { codebaseIndexProviderSchema } from "./codebase-index.js"
 
 export const providerNames = [
 	"anthropic",
+	"claude-code",
 	"glama",
 	"openrouter",
 	"bedrock",
@@ -86,6 +87,10 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
 	anthropicUseAuthToken: z.boolean().optional(),
 })
 
+const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
+	claudeCodePath: z.string().optional(),
+})
+
 const glamaSchema = baseProviderSettingsSchema.extend({
 	glamaModelId: z.string().optional(),
 	glamaApiKey: z.string().optional(),
@@ -235,6 +240,7 @@ const defaultSchema = z.object({
 
 export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
 	anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
+	claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
 	glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
 	openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
 	bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
@@ -264,6 +270,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 export const providerSettingsSchema = z.object({
 	apiProvider: providerNamesSchema.optional(),
 	...anthropicSchema.shape,
+	...claudeCodeSchema.shape,
 	...glamaSchema.shape,
 	...openRouterSchema.shape,
 	...bedrockSchema.shape,

+ 13 - 0
packages/types/src/providers/claude-code.ts

@@ -0,0 +1,13 @@
+import type { ModelInfo } from "../model.js"
+import { anthropicModels } from "./anthropic.js"
+
+// Claude Code
+export type ClaudeCodeModelId = keyof typeof claudeCodeModels
+export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
+export const claudeCodeModels = {
+	"claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"],
+	"claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"],
+	"claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"],
+	"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
+	"claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"],
+} as const satisfies Record<string, ModelInfo>

+ 3 - 3
packages/types/src/providers/gemini.ts

@@ -53,9 +53,9 @@ export const geminiModels = {
 		contextWindow: 1_048_576,
 		supportsImages: true,
 		supportsPromptCache: true,
-		inputPrice: 0.15,
-		outputPrice: 0.6,
-		cacheReadsPrice: 0.0375,
+		inputPrice: 0.3,
+		outputPrice: 2.5,
+		cacheReadsPrice: 0.075,
 		cacheWritesPrice: 1.0,
 		maxThinkingTokens: 24_576,
 		supportsReasoningBudget: true,

+ 2 - 0
packages/types/src/providers/index.ts

@@ -1,6 +1,7 @@
 export * from "./anthropic.js"
 export * from "./bedrock.js"
 export * from "./chutes.js"
+export * from "./claude-code.js"
 export * from "./deepseek.js"
 export * from "./gemini.js"
 export * from "./glama.js"
@@ -8,6 +9,7 @@ export * from "./groq.js"
 export * from "./lite-llm.js"
 export * from "./lm-studio.js"
 export * from "./mistral.js"
+export * from "./ollama.js"
 export * from "./openai.js"
 export * from "./openrouter.js"
 export * from "./requesty.js"

+ 18 - 0
packages/types/src/providers/lm-studio.ts

@@ -1 +1,19 @@
+import type { ModelInfo } from "../model.js"
+
 export const LMSTUDIO_DEFAULT_TEMPERATURE = 0
+
+// LM Studio
+// https://lmstudio.ai/docs/cli/ls
+export const lMStudioDefaultModelId = "mistralai/devstral-small-2505"
+export const lMStudioDefaultModelInfo: ModelInfo = {
+	maxTokens: 8192,
+	contextWindow: 200_000,
+	supportsImages: true,
+	supportsComputerUse: true,
+	supportsPromptCache: true,
+	inputPrice: 0,
+	outputPrice: 0,
+	cacheWritesPrice: 0,
+	cacheReadsPrice: 0,
+	description: "LM Studio hosted models",
+}

+ 17 - 0
packages/types/src/providers/ollama.ts

@@ -0,0 +1,17 @@
+import type { ModelInfo } from "../model.js"
+
+// Ollama
+// https://ollama.com/models
+export const ollamaDefaultModelId = "devstral:24b"
+export const ollamaDefaultModelInfo: ModelInfo = {
+	maxTokens: 4096,
+	contextWindow: 200_000,
+	supportsImages: true,
+	supportsComputerUse: true,
+	supportsPromptCache: true,
+	inputPrice: 0,
+	outputPrice: 0,
+	cacheWritesPrice: 0,
+	cacheReadsPrice: 0,
+	description: "Ollama hosted models",
+}

+ 4 - 2
packages/types/src/providers/vertex.ts

@@ -30,8 +30,10 @@ export const vertexModels = {
 		contextWindow: 1_048_576,
 		supportsImages: true,
 		supportsPromptCache: true,
-		inputPrice: 0.15,
-		outputPrice: 0.6,
+		inputPrice: 0.3,
+		outputPrice: 2.5,
+		cacheReadsPrice: 0.075,
+		cacheWritesPrice: 1.0,
 		maxThinkingTokens: 24_576,
 		supportsReasoningBudget: true,
 	},

+ 215 - 18
pnpm-lock.yaml

@@ -697,8 +697,11 @@ importers:
       '@google/genai':
         specifier: ^1.0.0
         version: 1.3.0(@modelcontextprotocol/[email protected])
+      '@lmstudio/sdk':
+        specifier: ^1.1.1
+        version: 1.2.2
       '@mistralai/mistralai':
-        specifier: ^1.6.0
+        specifier: ^1.3.6
         version: 1.7.1([email protected])
       '@modelcontextprotocol/sdk':
         specifier: ^1.9.0
@@ -818,8 +821,8 @@ importers:
         specifier: ^5.0.0
         version: 5.0.0
       pretty-bytes:
-        specifier: ^6.1.1
-        version: 6.1.1
+        specifier: ^7.0.0
+        version: 7.0.0
       ps-tree:
         specifier: ^1.2.0
         version: 1.2.0
@@ -839,8 +842,8 @@ importers:
         specifier: ^0.16.0
         version: 0.16.0
       serialize-error:
-        specifier: ^11.0.3
-        version: 11.0.3
+        specifier: ^12.0.0
+        version: 12.0.0
       simple-git:
         specifier: ^3.27.0
         version: 3.27.0
@@ -1079,6 +1082,9 @@ importers:
       i18next-http-backend:
         specifier: ^3.0.2
         version: 3.0.2
+      katex:
+        specifier: ^0.16.11
+        version: 0.16.22
       knuth-shuffle-seeded:
         specifier: ^1.0.6
         version: 1.0.6
@@ -1095,8 +1101,8 @@ importers:
         specifier: ^1.227.2
         version: 1.249.0
       pretty-bytes:
-        specifier: ^6.1.1
-        version: 6.1.1
+        specifier: ^7.0.0
+        version: 7.0.0
       react:
         specifier: ^18.3.1
         version: 18.3.1
@@ -1127,9 +1133,15 @@ importers:
       rehype-highlight:
         specifier: ^7.0.0
         version: 7.0.2
+      rehype-katex:
+        specifier: ^7.0.1
+        version: 7.0.1
       remark-gfm:
         specifier: ^4.0.1
         version: 4.0.1
+      remark-math:
+        specifier: ^6.0.0
+        version: 6.0.0
       remove-markdown:
         specifier: ^0.6.0
         version: 0.6.2
@@ -1146,8 +1158,8 @@ importers:
         specifier: ^6.1.13
         version: 6.1.18([email protected]([email protected]))([email protected])
       tailwind-merge:
-        specifier: ^2.6.0
-        version: 2.6.0
+        specifier: ^3.0.0
+        version: 3.3.0
       tailwindcss:
         specifier: ^4.0.0
         version: 4.1.8
@@ -1200,6 +1212,12 @@ importers:
       '@testing-library/user-event':
         specifier: ^14.6.1
         version: 14.6.1(@testing-library/[email protected])
+      '@types/jest':
+        specifier: ^29.0.0
+        version: 29.5.14
+      '@types/katex':
+        specifier: ^0.16.7
+        version: 0.16.7
       '@types/node':
         specifier: 20.x
         version: 20.17.57
@@ -2356,6 +2374,12 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@lmstudio/[email protected]':
+    resolution: {integrity: sha512-Or9KS1Iz3LC7D7WMe4zbqAqKOlDsVcrvMoQFBhmydzzxOg+eYBM5gtfgMMjcwjM0BuUVPhYOjTWEyfXpqfVJzg==}
+
+  '@lmstudio/[email protected]':
+    resolution: {integrity: sha512-9eXh7DnQKp4Puz/IZIkJJV04ZWZHPAJ3tR6Q8p0Hdbk3wR+UhLQxTc6ZM80XIbfa3MwDMx01XPvftmSr9k9KRQ==}
+
   '@manypkg/[email protected]':
     resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
 
@@ -4235,6 +4259,9 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==}
+
   '@types/[email protected]':
     resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==}
 
@@ -4244,6 +4271,9 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
+
   '@types/[email protected]':
     resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==}
 
@@ -6804,9 +6834,24 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==}
 
+  [email protected]:
+    resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
   [email protected]:
     resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
 
+  [email protected]:
+    resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
   [email protected]:
     resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
 
@@ -6819,6 +6864,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
   [email protected]:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -7609,6 +7657,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
 
+  [email protected]:
+    resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==}
+
   [email protected]:
     resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
     engines: {node: '>=12', npm: '>=6'}
@@ -8030,6 +8081,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
 
+  [email protected]:
+    resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
+
   [email protected]:
     resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
 
@@ -8115,6 +8169,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
 
+  [email protected]:
+    resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
+
   [email protected]:
     resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
 
@@ -8935,9 +8992,9 @@ packages:
     engines: {node: '>=14'}
     hasBin: true
 
-  pretty-bytes@6.1.1:
-    resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
-    engines: {node: ^14.13.1 || >=16.0.0}
+  pretty-bytes@7.0.0:
+    resolution: {integrity: sha512-U5otLYPR3L0SVjHGrkEUx5mf7MxV2ceXeE7VwWPk+hyzC5drNohsOGNPDZqxCqyX1lkbEN4kl1LiI8QFd7r0ZA==}
+    engines: {node: '>=20'}
 
   [email protected]:
     resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
@@ -9287,6 +9344,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==}
 
+  [email protected]:
+    resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
+
   [email protected]:
     resolution: {integrity: sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==}
 
@@ -9297,6 +9357,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
+
   [email protected]:
     resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
 
@@ -9489,9 +9552,9 @@ packages:
     resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
     engines: {node: '>= 18'}
 
-  serialize-error@11.0.3:
-    resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==}
-    engines: {node: '>=14.16'}
+  serialize-error@12.0.0:
+    resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==}
+    engines: {node: '>=18'}
 
   [email protected]:
     resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -10378,6 +10441,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
 
+  [email protected]:
+    resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
+
   [email protected]:
     resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==}
 
@@ -10531,6 +10597,9 @@ packages:
       react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
       react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
 
+  [email protected]:
+    resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
   [email protected]:
     resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==}
 
@@ -10680,6 +10749,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
   [email protected]:
     resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
     engines: {node: '>= 8'}
@@ -12645,6 +12717,24 @@ snapshots:
   '@libsql/[email protected]':
     optional: true
 
+  '@lmstudio/[email protected]':
+    dependencies:
+      ws: 8.18.2
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  '@lmstudio/[email protected]':
+    dependencies:
+      '@lmstudio/lms-isomorphic': 0.4.5
+      chalk: 4.1.2
+      jsonschema: 1.5.0
+      zod: 3.25.67
+      zod-to-json-schema: 3.24.5([email protected])
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   '@manypkg/[email protected]':
     dependencies:
       '@babel/runtime': 7.27.6
@@ -14778,6 +14868,11 @@ snapshots:
     dependencies:
       '@types/istanbul-lib-report': 3.0.3
 
+  '@types/[email protected]':
+    dependencies:
+      expect: 29.7.0
+      pretty-format: 29.7.0
+
   '@types/[email protected]': {}
 
   '@types/[email protected]':
@@ -14788,6 +14883,8 @@ snapshots:
 
   '@types/[email protected]': {}
 
+  '@types/[email protected]': {}
+
   '@types/[email protected]':
     dependencies:
       '@types/lodash': 4.17.17
@@ -17684,10 +17781,47 @@ snapshots:
       unist-util-is: 4.1.0
       web-namespaces: 1.1.4
 
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      hastscript: 9.0.1
+      web-namespaces: 2.0.1
+
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      hast-util-from-dom: 5.0.1
+      hast-util-from-html: 2.0.3
+      unist-util-remove-position: 5.0.0
+
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      devlop: 1.1.0
+      hast-util-from-parse5: 8.0.3
+      parse5: 7.3.0
+      vfile: 6.0.3
+      vfile-message: 4.0.2
+
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/unist': 3.0.3
+      devlop: 1.1.0
+      hastscript: 9.0.1
+      property-information: 7.1.0
+      vfile: 6.0.3
+      vfile-location: 5.0.3
+      web-namespaces: 2.0.1
+
   [email protected]:
     dependencies:
       '@types/hast': 3.0.4
 
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+
   [email protected]:
     dependencies:
       '@types/hast': 3.0.4
@@ -17733,6 +17867,14 @@ snapshots:
     dependencies:
       '@types/hast': 3.0.4
 
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      comma-separated-tokens: 2.0.3
+      hast-util-parse-selector: 4.0.0
+      property-information: 7.1.0
+      space-separated-tokens: 2.0.2
+
   [email protected]: {}
 
   [email protected]: {}
@@ -18813,6 +18955,8 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       jws: 3.2.2
@@ -19315,6 +19459,18 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/mdast': 4.0.4
+      devlop: 1.1.0
+      longest-streak: 3.1.0
+      mdast-util-from-markdown: 2.0.2
+      mdast-util-to-markdown: 2.1.2
+      unist-util-remove-position: 5.0.0
+    transitivePeerDependencies:
+      - supports-color
+
   [email protected]:
     dependencies:
       '@types/estree-jsx': 1.0.5
@@ -19522,6 +19678,16 @@ snapshots:
       micromark-util-combine-extensions: 2.0.1
       micromark-util-types: 2.0.2
 
+  [email protected]:
+    dependencies:
+      '@types/katex': 0.16.7
+      devlop: 1.1.0
+      katex: 0.16.22
+      micromark-factory-space: 2.0.1
+      micromark-util-character: 2.1.1
+      micromark-util-symbol: 2.0.1
+      micromark-util-types: 2.0.2
+
   [email protected]:
     dependencies:
       micromark-util-character: 2.1.1
@@ -20439,7 +20605,7 @@ snapshots:
 
   [email protected]: {}
 
-  pretty-bytes@6.1.1: {}
+  pretty-bytes@7.0.0: {}
 
   [email protected]:
     dependencies:
@@ -20905,6 +21071,16 @@ snapshots:
       unist-util-visit: 5.0.0
       vfile: 6.0.3
 
+  [email protected]:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/katex': 0.16.7
+      hast-util-from-html-isomorphic: 2.0.0
+      hast-util-to-text: 4.0.2
+      katex: 0.16.22
+      unist-util-visit-parents: 6.0.1
+      vfile: 6.0.3
+
   [email protected]:
     dependencies:
       '@mapbox/hast-util-table-cell-style': 0.2.1
@@ -20925,6 +21101,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  [email protected]:
+    dependencies:
+      '@types/mdast': 4.0.4
+      mdast-util-math: 3.0.0
+      micromark-extension-math: 3.1.0
+      unified: 11.0.5
+    transitivePeerDependencies:
+      - supports-color
+
   [email protected]:
     dependencies:
       '@types/mdast': 4.0.4
@@ -21157,9 +21342,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  serialize-error@11.0.3:
+  serialize-error@12.0.0:
     dependencies:
-      type-fest: 2.19.0
+      type-fest: 4.41.0
 
   [email protected]:
     dependencies:
@@ -22152,6 +22337,11 @@ snapshots:
     dependencies:
       '@types/unist': 3.0.3
 
+  [email protected]:
+    dependencies:
+      '@types/unist': 3.0.3
+      unist-util-visit: 5.0.0
+
   [email protected]:
     dependencies:
       '@types/unist': 2.0.11
@@ -22309,6 +22499,11 @@ snapshots:
       - '@types/react'
       - '@types/react-dom'
 
+  [email protected]:
+    dependencies:
+      '@types/unist': 3.0.3
+      vfile: 6.0.3
+
   [email protected]:
     dependencies:
       '@types/unist': 2.0.11
@@ -22570,6 +22765,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     optional: true
 

+ 3 - 0
src/.vscodeignore

@@ -19,6 +19,9 @@
 !webview-ui/build/assets/*.js
 !webview-ui/build/assets/*.ttf
 !webview-ui/build/assets/*.css
+!webview-ui/build/assets/fonts/*.woff
+!webview-ui/build/assets/fonts/*.woff2
+!webview-ui/build/assets/fonts/*.ttf
 
 # Include default themes JSON files used in getTheme
 !integrations/theme/default-themes/**

+ 1 - 0
src/__mocks__/fs/promises.ts

@@ -168,6 +168,7 @@ const mockFs = {
 						args: ["test.js"],
 						disabled: false,
 						alwaysAllow: ["existing-tool"],
+						disabledTools: [],
 					},
 				},
 			}),

+ 7 - 1
src/activate/handleUri.ts

@@ -45,7 +45,13 @@ export const handleUri = async (uri: vscode.Uri) => {
 		case "/auth/clerk/callback": {
 			const code = query.get("code")
 			const state = query.get("state")
-			await CloudService.instance.handleAuthCallback(code, state)
+			const organizationId = query.get("organizationId")
+
+			await CloudService.instance.handleAuthCallback(
+				code,
+				state,
+				organizationId === "null" ? null : organizationId,
+			)
 			break
 		}
 		default:

+ 3 - 0
src/api/index.ts

@@ -28,6 +28,7 @@ import {
 	ChutesHandler,
 	LiteLLMHandler,
 	CerebrasHandler, // kilocode_change
+	ClaudeCodeHandler,
 } from "./providers"
 // kilocode_change start
 import { FireworksHandler } from "./providers/fireworks"
@@ -71,6 +72,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new KilocodeOpenrouterHandler(options)
 		case "anthropic":
 			return new AnthropicHandler(options)
+		case "claude-code":
+			return new ClaudeCodeHandler(options)
 		case "glama":
 			return new GlamaHandler(options)
 		case "openrouter":

+ 230 - 0
src/api/providers/__tests__/claude-code.spec.ts

@@ -0,0 +1,230 @@
+import { describe, test, expect, vi, beforeEach } from "vitest"
+import { ClaudeCodeHandler } from "../claude-code"
+import { ApiHandlerOptions } from "../../../shared/api"
+
+// Mock the runClaudeCode function
+vi.mock("../../../integrations/claude-code/run", () => ({
+	runClaudeCode: vi.fn(),
+}))
+
+const { runClaudeCode } = await import("../../../integrations/claude-code/run")
+const mockRunClaudeCode = vi.mocked(runClaudeCode)
+
+// Mock the EventEmitter for the process
+class MockEventEmitter {
+	private handlers: { [event: string]: ((...args: any[]) => void)[] } = {}
+
+	on(event: string, handler: (...args: any[]) => void) {
+		if (!this.handlers[event]) {
+			this.handlers[event] = []
+		}
+		this.handlers[event].push(handler)
+	}
+
+	emit(event: string, ...args: any[]) {
+		if (this.handlers[event]) {
+			this.handlers[event].forEach((handler) => handler(...args))
+		}
+	}
+}
+
+describe("ClaudeCodeHandler", () => {
+	let handler: ClaudeCodeHandler
+	let mockProcess: any
+
+	beforeEach(() => {
+		const options: ApiHandlerOptions = {
+			claudeCodePath: "claude",
+			apiModelId: "claude-3-5-sonnet-20241022",
+		}
+		handler = new ClaudeCodeHandler(options)
+
+		const mainEmitter = new MockEventEmitter()
+		mockProcess = {
+			stdout: new MockEventEmitter(),
+			stderr: new MockEventEmitter(),
+			on: mainEmitter.on.bind(mainEmitter),
+			emit: mainEmitter.emit.bind(mainEmitter),
+		}
+
+		mockRunClaudeCode.mockReturnValue(mockProcess)
+	})
+
+	test("should handle thinking content properly", async () => {
+		const systemPrompt = "You are a helpful assistant"
+		const messages = [{ role: "user" as const, content: "Hello" }]
+
+		// Start the stream
+		const stream = handler.createMessage(systemPrompt, messages)
+		const streamGenerator = stream[Symbol.asyncIterator]()
+
+		// Simulate thinking content response
+		const thinkingResponse = {
+			type: "assistant",
+			message: {
+				id: "msg_123",
+				type: "message",
+				role: "assistant",
+				model: "claude-3-5-sonnet-20241022",
+				content: [
+					{
+						type: "thinking",
+						thinking: "I need to think about this carefully...",
+						signature: "abc123",
+					},
+				],
+				stop_reason: null,
+				stop_sequence: null,
+				usage: {
+					input_tokens: 10,
+					output_tokens: 20,
+					service_tier: "standard" as const,
+				},
+			},
+			session_id: "session_123",
+		}
+
+		// Emit the thinking response and wait for processing
+		setImmediate(() => {
+			mockProcess.stdout.emit("data", JSON.stringify(thinkingResponse) + "\n")
+			setImmediate(() => {
+				mockProcess.emit("close", 0)
+			})
+		})
+
+		// Get the result
+		const result = await streamGenerator.next()
+
+		expect(result.done).toBe(false)
+		expect(result.value).toEqual({
+			type: "reasoning",
+			text: "I need to think about this carefully...",
+		})
+	})
+
+	test("should handle mixed content types", async () => {
+		const systemPrompt = "You are a helpful assistant"
+		const messages = [{ role: "user" as const, content: "Hello" }]
+
+		const stream = handler.createMessage(systemPrompt, messages)
+		const streamGenerator = stream[Symbol.asyncIterator]()
+
+		// Simulate mixed content response
+		const mixedResponse = {
+			type: "assistant",
+			message: {
+				id: "msg_123",
+				type: "message",
+				role: "assistant",
+				model: "claude-3-5-sonnet-20241022",
+				content: [
+					{
+						type: "thinking",
+						thinking: "Let me think about this...",
+					},
+					{
+						type: "text",
+						text: "Here's my response!",
+					},
+				],
+				stop_reason: null,
+				stop_sequence: null,
+				usage: {
+					input_tokens: 10,
+					output_tokens: 20,
+					service_tier: "standard" as const,
+				},
+			},
+			session_id: "session_123",
+		}
+
+		// Emit the mixed response and wait for processing
+		setImmediate(() => {
+			mockProcess.stdout.emit("data", JSON.stringify(mixedResponse) + "\n")
+			setImmediate(() => {
+				mockProcess.emit("close", 0)
+			})
+		})
+
+		// Get the first result (thinking)
+		const thinkingResult = await streamGenerator.next()
+		expect(thinkingResult.done).toBe(false)
+		expect(thinkingResult.value).toEqual({
+			type: "reasoning",
+			text: "Let me think about this...",
+		})
+
+		// Get the second result (text)
+		const textResult = await streamGenerator.next()
+		expect(textResult.done).toBe(false)
+		expect(textResult.value).toEqual({
+			type: "text",
+			text: "Here's my response!",
+		})
+	})
+
+	test("should handle stop_reason with thinking content in error messages", async () => {
+		const systemPrompt = "You are a helpful assistant"
+		const messages = [{ role: "user" as const, content: "Hello" }]
+
+		const stream = handler.createMessage(systemPrompt, messages)
+		const streamGenerator = stream[Symbol.asyncIterator]()
+
+		// Simulate error response with thinking content
+		const errorResponse = {
+			type: "assistant",
+			message: {
+				id: "msg_123",
+				type: "message",
+				role: "assistant",
+				model: "claude-3-5-sonnet-20241022",
+				content: [
+					{
+						type: "thinking",
+						thinking: "This is an error scenario",
+					},
+				],
+				stop_reason: "max_tokens",
+				stop_sequence: null,
+				usage: {
+					input_tokens: 10,
+					output_tokens: 20,
+					service_tier: "standard" as const,
+				},
+			},
+			session_id: "session_123",
+		}
+
+		// Emit the error response and wait for processing
+		setImmediate(() => {
+			mockProcess.stdout.emit("data", JSON.stringify(errorResponse) + "\n")
+			setImmediate(() => {
+				mockProcess.emit("close", 0)
+			})
+		})
+
+		// Should throw error with thinking content
+		await expect(streamGenerator.next()).rejects.toThrow("This is an error scenario")
+	})
+
+	test("should handle incomplete JSON in buffer on process close", async () => {
+		const systemPrompt = "You are a helpful assistant"
+		const messages = [{ role: "user" as const, content: "Hello" }]
+
+		const stream = handler.createMessage(systemPrompt, messages)
+		const streamGenerator = stream[Symbol.asyncIterator]()
+
+		// Simulate incomplete JSON data followed by process close
+		setImmediate(() => {
+			// Send incomplete JSON (missing closing brace)
+			mockProcess.stdout.emit("data", '{"type":"assistant","message":{"id":"msg_123"')
+			setImmediate(() => {
+				mockProcess.emit("close", 0)
+			})
+		})
+
+		// Should complete without throwing, incomplete JSON should be discarded
+		const result = await streamGenerator.next()
+		expect(result.done).toBe(true)
+	})
+})

+ 240 - 0
src/api/providers/claude-code.ts

@@ -0,0 +1,240 @@
+import type { Anthropic } from "@anthropic-ai/sdk"
+import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } from "@roo-code/types"
+import { type ApiHandler } from ".."
+import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
+import { runClaudeCode } from "../../integrations/claude-code/run"
+import { ClaudeCodeMessage } from "../../integrations/claude-code/types"
+import { BaseProvider } from "./base-provider"
+import { t } from "../../i18n"
+import { ApiHandlerOptions } from "../../shared/api"
+
+export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
+	private options: ApiHandlerOptions
+
+	constructor(options: ApiHandlerOptions) {
+		super()
+		this.options = options
+	}
+
+	override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+		const claudeProcess = runClaudeCode({
+			systemPrompt,
+			messages,
+			path: this.options.claudeCodePath,
+			modelId: this.getModel().id,
+		})
+
+		const dataQueue: string[] = []
+		let processError = null
+		let errorOutput = ""
+		let exitCode: number | null = null
+		let buffer = ""
+
+		claudeProcess.stdout.on("data", (data) => {
+			buffer += data.toString()
+			const lines = buffer.split("\n")
+
+			// Keep the last line in buffer as it might be incomplete
+			buffer = lines.pop() || ""
+
+			// Process complete lines
+			for (const line of lines) {
+				const trimmedLine = line.trim()
+				if (trimmedLine !== "") {
+					dataQueue.push(trimmedLine)
+				}
+			}
+		})
+
+		claudeProcess.stderr.on("data", (data) => {
+			errorOutput += data.toString()
+		})
+
+		claudeProcess.on("close", (code) => {
+			exitCode = code
+			// Process any remaining data in buffer
+			const trimmedBuffer = buffer.trim()
+			if (trimmedBuffer) {
+				// Validate that the remaining buffer looks like valid JSON before processing
+				if (this.isLikelyValidJSON(trimmedBuffer)) {
+					dataQueue.push(trimmedBuffer)
+				} else {
+					console.warn(
+						"Discarding incomplete JSON data on process close:",
+						trimmedBuffer.substring(0, 100) + (trimmedBuffer.length > 100 ? "..." : ""),
+					)
+				}
+				buffer = ""
+			}
+		})
+
+		claudeProcess.on("error", (error) => {
+			processError = error
+		})
+
+		// Usage is included with assistant messages,
+		// but cost is included in the result chunk
+		let usage: ApiStreamUsageChunk = {
+			type: "usage",
+			inputTokens: 0,
+			outputTokens: 0,
+			cacheReadTokens: 0,
+			cacheWriteTokens: 0,
+		}
+
+		while (exitCode !== 0 || dataQueue.length > 0) {
+			if (dataQueue.length === 0) {
+				await new Promise((resolve) => setImmediate(resolve))
+			}
+
+			if (exitCode !== null && exitCode !== 0) {
+				if (errorOutput) {
+					throw new Error(
+						t("common:errors.claudeCode.processExitedWithError", {
+							exitCode,
+							output: errorOutput.trim(),
+						}),
+					)
+				}
+				throw new Error(t("common:errors.claudeCode.processExited", { exitCode }))
+			}
+
+			const data = dataQueue.shift()
+			if (!data) {
+				continue
+			}
+
+			const chunk = this.attemptParseChunk(data)
+
+			if (!chunk) {
+				yield {
+					type: "text",
+					text: data || "",
+				}
+
+				continue
+			}
+
+			if (chunk.type === "system" && chunk.subtype === "init") {
+				continue
+			}
+
+			if (chunk.type === "assistant" && "message" in chunk) {
+				const message = chunk.message
+
+				if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
+					const firstContent = message.content[0]
+					const errorMessage =
+						this.getContentText(firstContent) ||
+						t("common:errors.claudeCode.stoppedWithReason", { reason: message.stop_reason })
+
+					if (errorMessage.includes("Invalid model name")) {
+						throw new Error(errorMessage + `\n\n${t("common:errors.claudeCode.apiKeyModelPlanMismatch")}`)
+					}
+
+					throw new Error(errorMessage)
+				}
+
+				for (const content of message.content) {
+					if (content.type === "text") {
+						yield {
+							type: "text",
+							text: content.text,
+						}
+					} else if (content.type === "thinking") {
+						yield {
+							type: "reasoning",
+							text: content.thinking,
+						}
+					} else {
+						console.warn("Unsupported content type:", content)
+					}
+				}
+
+				usage.inputTokens += message.usage.input_tokens
+				usage.outputTokens += message.usage.output_tokens
+				usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
+				usage.cacheWriteTokens =
+					(usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
+
+				continue
+			}
+
+			if (chunk.type === "result" && "result" in chunk) {
+				// Only use the cost from the CLI if provided
+				// Don't calculate cost as it may be $0 for subscription users
+				usage.totalCost = chunk.cost_usd ?? 0
+
+				yield usage
+			}
+
+			if (processError) {
+				throw processError
+			}
+		}
+	}
+
+	getModel() {
+		const modelId = this.options.apiModelId
+		if (modelId && modelId in claudeCodeModels) {
+			const id = modelId as ClaudeCodeModelId
+			return { id, info: claudeCodeModels[id] }
+		}
+
+		return {
+			id: claudeCodeDefaultModelId,
+			info: claudeCodeModels[claudeCodeDefaultModelId],
+		}
+	}
+
+	private getContentText(content: any): string | undefined {
+		if (!content) return undefined
+		switch (content.type) {
+			case "text":
+				return content.text
+			case "thinking":
+				return content.thinking
+			default:
+				return undefined
+		}
+	}
+
+	private isLikelyValidJSON(data: string): boolean {
+		// Basic validation to check if the data looks like it could be valid JSON
+		const trimmed = data.trim()
+		if (!trimmed) return false
+
+		// Must start and end with appropriate JSON delimiters
+		const startsCorrectly = trimmed.startsWith("{") || trimmed.startsWith("[")
+		const endsCorrectly = trimmed.endsWith("}") || trimmed.endsWith("]")
+
+		if (!startsCorrectly || !endsCorrectly) return false
+
+		// Check for balanced braces/brackets (simple heuristic)
+		let braceCount = 0
+		let bracketCount = 0
+		for (const char of trimmed) {
+			if (char === "{") braceCount++
+			else if (char === "}") braceCount--
+			else if (char === "[") bracketCount++
+			else if (char === "]") bracketCount--
+		}
+
+		return braceCount === 0 && bracketCount === 0
+	}
+
+	// TODO: Validate instead of parsing
+	private attemptParseChunk(data: string): ClaudeCodeMessage | null {
+		try {
+			return JSON.parse(data)
+		} catch (error) {
+			console.error(
+				"Error parsing chunk:",
+				error,
+				"Data:",
+				data.substring(0, 100) + (data.length > 100 ? "..." : ""),
+			)
+			return null
+		}
+	}
+}

+ 14 - 0
src/api/providers/fetchers/__tests__/fixtures/lmstudio-model-details.json

@@ -0,0 +1,14 @@
+{
+	"mistralai/devstral-small-2505": {
+		"type": "llm",
+		"modelKey": "mistralai/devstral-small-2505",
+		"format": "safetensors",
+		"displayName": "Devstral Small 2505",
+		"path": "mistralai/devstral-small-2505",
+		"sizeBytes": 13277565112,
+		"architecture": "mistral",
+		"vision": false,
+		"trainedForToolUse": false,
+		"maxContextLength": 131072
+	}
+}

+ 58 - 0
src/api/providers/fetchers/__tests__/fixtures/ollama-model-details.json

@@ -0,0 +1,58 @@
+{
+	"qwen3-2to16:latest": {
+		"license": "                                 Apache License\\n                           Version 2.0, January 2004\\n...",
+		"modelfile": "model.modelfile,# To build a new Modelfile based on this, replace FROM with:...",
+		"parameters": "repeat_penalty                 1\\nstop                           \\\\nstop...",
+		"template": "{{- if .Messages }}\\n{{- if or .System .Tools }}<|im_start|>system...",
+		"details": {
+			"parent_model": "/Users/brad/.ollama/models/blobs/sha256-3291abe70f16ee9682de7bfae08db5373ea9d6497e614aaad63340ad421d6312",
+			"format": "gguf",
+			"family": "qwen3",
+			"families": ["qwen3"],
+			"parameter_size": "32.8B",
+			"quantization_level": "Q4_K_M"
+		},
+		"model_info": {
+			"general.architecture": "qwen3",
+			"general.basename": "Qwen3",
+			"general.file_type": 15,
+			"general.parameter_count": 32762123264,
+			"general.quantization_version": 2,
+			"general.size_label": "32B",
+			"general.type": "model",
+			"qwen3.attention.head_count": 64,
+			"qwen3.attention.head_count_kv": 8,
+			"qwen3.attention.key_length": 128,
+			"qwen3.attention.layer_norm_rms_epsilon": 0.000001,
+			"qwen3.attention.value_length": 128,
+			"qwen3.block_count": 64,
+			"qwen3.context_length": 40960,
+			"qwen3.embedding_length": 5120,
+			"qwen3.feed_forward_length": 25600,
+			"qwen3.rope.freq_base": 1000000,
+			"tokenizer.ggml.add_bos_token": false,
+			"tokenizer.ggml.bos_token_id": 151643,
+			"tokenizer.ggml.eos_token_id": 151645,
+			"tokenizer.ggml.merges": null,
+			"tokenizer.ggml.model": "gpt2",
+			"tokenizer.ggml.padding_token_id": 151643,
+			"tokenizer.ggml.pre": "qwen2",
+			"tokenizer.ggml.token_type": null,
+			"tokenizer.ggml.tokens": null
+		},
+		"tensors": [
+			{
+				"name": "output.weight",
+				"type": "Q6_K",
+				"shape": [5120, 151936]
+			},
+			{
+				"name": "output_norm.weight",
+				"type": "F32",
+				"shape": [5120]
+			}
+		],
+		"capabilities": ["completion", "tools"],
+		"modified_at": "2025-06-02T22:16:13.644123606-04:00"
+	}
+}

+ 235 - 0
src/api/providers/fetchers/__tests__/lmstudio.test.ts

@@ -0,0 +1,235 @@
+import axios from "axios"
+import { vi, describe, it, expect, beforeEach } from "vitest"
+import { LMStudioClient, LLM, LLMInstanceInfo, LLMInfo } from "@lmstudio/sdk"
+import { getLMStudioModels, parseLMStudioModel } from "../lmstudio"
+import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" // ModelInfo is a type
+
+// Mock axios
+vi.mock("axios")
+const mockedAxios = axios as any
+
+// Mock @lmstudio/sdk
+const mockGetModelInfo = vi.fn()
+const mockListLoaded = vi.fn()
+const mockListDownloadedModels = vi.fn()
+vi.mock("@lmstudio/sdk", () => {
+	return {
+		LMStudioClient: vi.fn().mockImplementation(() => ({
+			llm: {
+				listLoaded: mockListLoaded,
+			},
+			system: {
+				listDownloadedModels: mockListDownloadedModels,
+			},
+		})),
+	}
+})
+const MockedLMStudioClientConstructor = LMStudioClient as any
+
+describe("LMStudio Fetcher", () => {
+	beforeEach(() => {
+		vi.clearAllMocks()
+		MockedLMStudioClientConstructor.mockClear()
+		mockListLoaded.mockClear()
+		mockGetModelInfo.mockClear()
+		mockListDownloadedModels.mockClear()
+	})
+
+	describe("parseLMStudioModel", () => {
+		it("should correctly parse raw LLMInfo to ModelInfo", () => {
+			const rawModel: LLMInstanceInfo = {
+				type: "llm",
+				modelKey: "mistralai/devstral-small-2505",
+				format: "safetensors",
+				displayName: "Devstral Small 2505",
+				path: "mistralai/devstral-small-2505",
+				sizeBytes: 13277565112,
+				architecture: "mistral",
+				identifier: "mistralai/devstral-small-2505",
+				instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ",
+				vision: false,
+				trainedForToolUse: false,
+				maxContextLength: 131072,
+				contextLength: 7161,
+			}
+
+			const expectedModelInfo: ModelInfo = {
+				...lMStudioDefaultModelInfo,
+				description: `${rawModel.displayName} - ${rawModel.path}`,
+				contextWindow: rawModel.contextLength,
+				supportsPromptCache: true,
+				supportsImages: rawModel.vision,
+				supportsComputerUse: false,
+				maxTokens: rawModel.contextLength,
+				inputPrice: 0,
+				outputPrice: 0,
+				cacheWritesPrice: 0,
+				cacheReadsPrice: 0,
+			}
+
+			const result = parseLMStudioModel(rawModel)
+			expect(result).toEqual(expectedModelInfo)
+		})
+	})
+
+	describe("getLMStudioModels", () => {
+		const baseUrl = "http://localhost:1234"
+		const lmsUrl = "ws://localhost:1234"
+
+		const mockRawModel: LLMInstanceInfo = {
+			architecture: "test-arch",
+			identifier: "mistralai/devstral-small-2505",
+			instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ",
+			modelKey: "test-model-key-1",
+			path: "/path/to/test-model-1",
+			type: "llm",
+			displayName: "Test Model One",
+			maxContextLength: 2048,
+			contextLength: 7161,
+			paramsString: "1B params, 2k context",
+			vision: true,
+			format: "gguf",
+			sizeBytes: 1000000000,
+			trainedForToolUse: false, // Added
+		}
+
+		it("should fetch downloaded models using system.listDownloadedModels", async () => {
+			const mockLLMInfo: LLMInfo = {
+				type: "llm" as const,
+				modelKey: "mistralai/devstral-small-2505",
+				format: "safetensors",
+				displayName: "Devstral Small 2505",
+				path: "mistralai/devstral-small-2505",
+				sizeBytes: 13277565112,
+				architecture: "mistral",
+				vision: false,
+				trainedForToolUse: false,
+				maxContextLength: 131072,
+			}
+
+			mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
+			mockListDownloadedModels.mockResolvedValueOnce([mockLLMInfo])
+
+			const result = await getLMStudioModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
+			expect(mockListDownloadedModels).toHaveBeenCalledTimes(1)
+			expect(mockListDownloadedModels).toHaveBeenCalledWith("llm")
+			expect(mockListLoaded).not.toHaveBeenCalled()
+
+			const expectedParsedModel = parseLMStudioModel(mockLLMInfo)
+			expect(result).toEqual({ [mockLLMInfo.path]: expectedParsedModel })
+		})
+
+		it("should fall back to listLoaded when listDownloadedModels fails", async () => {
+			mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
+			mockListDownloadedModels.mockRejectedValueOnce(new Error("Method not available"))
+			mockListLoaded.mockResolvedValueOnce([{ getModelInfo: mockGetModelInfo }])
+			mockGetModelInfo.mockResolvedValueOnce(mockRawModel)
+
+			const result = await getLMStudioModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
+			expect(mockListDownloadedModels).toHaveBeenCalledTimes(1)
+			expect(mockListLoaded).toHaveBeenCalledTimes(1)
+
+			const expectedParsedModel = parseLMStudioModel(mockRawModel)
+			expect(result).toEqual({ [mockRawModel.modelKey]: expectedParsedModel })
+		})
+
+		it("should use default baseUrl if an empty string is provided", async () => {
+			const defaultBaseUrl = "http://localhost:1234"
+			const defaultLmsUrl = "ws://localhost:1234"
+			mockedAxios.get.mockResolvedValueOnce({ data: {} })
+			mockListLoaded.mockResolvedValueOnce([])
+
+			await getLMStudioModels("")
+
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${defaultBaseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: defaultLmsUrl })
+		})
+
+		it("should transform https baseUrl to wss for LMStudioClient", async () => {
+			const httpsBaseUrl = "https://securehost:4321"
+			const wssLmsUrl = "wss://securehost:4321"
+			mockedAxios.get.mockResolvedValueOnce({ data: {} })
+			mockListLoaded.mockResolvedValueOnce([])
+
+			await getLMStudioModels(httpsBaseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${httpsBaseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: wssLmsUrl })
+		})
+
+		it("should return an empty object if lmsUrl is unparsable", async () => {
+			const unparsableBaseUrl = "http://localhost:invalid:port" // Leads to ws://localhost:invalid:port
+
+			const result = await getLMStudioModels(unparsableBaseUrl)
+
+			expect(result).toEqual({})
+			expect(mockedAxios.get).not.toHaveBeenCalled()
+			expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
+		})
+
+		it("should return an empty object and log error if axios.get fails with a generic error", async () => {
+			const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
+			const networkError = new Error("Network connection failed")
+			mockedAxios.get.mockRejectedValueOnce(networkError)
+
+			const result = await getLMStudioModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
+			expect(mockListLoaded).not.toHaveBeenCalled()
+			expect(consoleErrorSpy).toHaveBeenCalledWith(
+				`Error fetching LMStudio models: ${JSON.stringify(networkError, Object.getOwnPropertyNames(networkError), 2)}`,
+			)
+			expect(result).toEqual({})
+			consoleErrorSpy.mockRestore()
+		})
+
+		it("should return an empty object and log info if axios.get fails with ECONNREFUSED", async () => {
+			const consoleInfoSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
+			const econnrefusedError = new Error("Connection refused")
+			;(econnrefusedError as any).code = "ECONNREFUSED"
+			mockedAxios.get.mockRejectedValueOnce(econnrefusedError)
+
+			const result = await getLMStudioModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
+			expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
+			expect(mockListLoaded).not.toHaveBeenCalled()
+			expect(consoleInfoSpy).toHaveBeenCalledWith(`Error connecting to LMStudio at ${baseUrl}`)
+			expect(result).toEqual({})
+			consoleInfoSpy.mockRestore()
+		})
+
+		it("should return an empty object and log error if listDownloadedModels fails", async () => {
+			const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
+			const listError = new Error("LMStudio SDK internal error")
+
+			mockedAxios.get.mockResolvedValueOnce({ data: {} })
+			mockListLoaded.mockRejectedValueOnce(listError)
+
+			const result = await getLMStudioModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
+			expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
+			expect(mockListLoaded).toHaveBeenCalledTimes(1)
+			expect(consoleErrorSpy).toHaveBeenCalledWith(
+				`Error fetching LMStudio models: ${JSON.stringify(listError, Object.getOwnPropertyNames(listError), 2)}`,
+			)
+			expect(result).toEqual({})
+			consoleErrorSpy.mockRestore()
+		})
+	})
+})

+ 222 - 0
src/api/providers/fetchers/__tests__/ollama.test.ts

@@ -0,0 +1,222 @@
+import axios from "axios"
+import path from "path"
+import { vi, describe, it, expect, beforeEach } from "vitest"
+import { getOllamaModels, parseOllamaModel } from "../ollama"
+import ollamaModelsData from "./fixtures/ollama-model-details.json"
+
+// Mock axios
+vi.mock("axios")
+const mockedAxios = axios as any
+
+describe("Ollama Fetcher", () => {
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("parseOllamaModel", () => {
+		it("should correctly parse Ollama model info", () => {
+			const modelData = ollamaModelsData["qwen3-2to16:latest"]
+			const parsedModel = parseOllamaModel(modelData)
+
+			expect(parsedModel).toEqual({
+				maxTokens: 40960,
+				contextWindow: 40960,
+				supportsImages: false,
+				supportsComputerUse: false,
+				supportsPromptCache: true,
+				inputPrice: 0,
+				outputPrice: 0,
+				cacheWritesPrice: 0,
+				cacheReadsPrice: 0,
+				description: "Family: qwen3, Context: 40960, Size: 32.8B",
+			})
+		})
+
+		it("should handle models with null families field", () => {
+			const modelDataWithNullFamilies = {
+				...ollamaModelsData["qwen3-2to16:latest"],
+				details: {
+					...ollamaModelsData["qwen3-2to16:latest"].details,
+					families: null,
+				},
+			}
+
+			const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any)
+
+			expect(parsedModel).toEqual({
+				maxTokens: 40960,
+				contextWindow: 40960,
+				supportsImages: false,
+				supportsComputerUse: false,
+				supportsPromptCache: true,
+				inputPrice: 0,
+				outputPrice: 0,
+				cacheWritesPrice: 0,
+				cacheReadsPrice: 0,
+				description: "Family: qwen3, Context: 40960, Size: 32.8B",
+			})
+		})
+	})
+
+	describe("getOllamaModels", () => {
+		it("should fetch model list from /api/tags and details for each model from /api/show", async () => {
+			const baseUrl = "http://localhost:11434"
+			const modelName = "devstral2to16:latest"
+
+			const mockApiTagsResponse = {
+				models: [
+					{
+						name: modelName,
+						model: modelName,
+						modified_at: "2025-06-03T09:23:22.610222878-04:00",
+						size: 14333928010,
+						digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
+						details: {
+							family: "llama",
+							families: ["llama"],
+							format: "gguf",
+							parameter_size: "23.6B",
+							parent_model: "",
+							quantization_level: "Q4_K_M",
+						},
+					},
+				],
+			}
+			const mockApiShowResponse = {
+				license: "Mock License",
+				modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
+				parameters: "num_ctx 4096\nstop_token <eos>",
+				template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
+				modified_at: "2025-06-03T09:23:22.610222878-04:00",
+				details: {
+					parent_model: "",
+					format: "gguf",
+					family: "llama",
+					families: ["llama"],
+					parameter_size: "23.6B",
+					quantization_level: "Q4_K_M",
+				},
+				model_info: {
+					"ollama.context_length": 4096,
+					"some.other.info": "value",
+				},
+				capabilities: ["completion"],
+			}
+
+			mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
+			mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
+
+			const result = await getOllamaModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+
+			expect(mockedAxios.post).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
+
+			expect(typeof result).toBe("object")
+			expect(result).not.toBeInstanceOf(Array)
+			expect(Object.keys(result).length).toBe(1)
+			expect(result[modelName]).toBeDefined()
+
+			const expectedParsedDetails = parseOllamaModel(mockApiShowResponse as any)
+			expect(result[modelName]).toEqual(expectedParsedDetails)
+		})
+
+		it("should return an empty list if the initial /api/tags call fails", async () => {
+			const baseUrl = "http://localhost:11434"
+			mockedAxios.get.mockRejectedValueOnce(new Error("Network error"))
+			const consoleInfoSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Spy and suppress output
+
+			const result = await getOllamaModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.post).not.toHaveBeenCalled()
+			expect(result).toEqual({})
+		})
+
+		it("should log an info message and return an empty object on ECONNREFUSED", async () => {
+			const baseUrl = "http://localhost:11434"
+			const consoleInfoSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) // Spy and suppress output
+
+			const econnrefusedError = new Error("Connection refused") as any
+			econnrefusedError.code = "ECONNREFUSED"
+			mockedAxios.get.mockRejectedValueOnce(econnrefusedError)
+
+			const result = await getOllamaModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+			expect(mockedAxios.post).not.toHaveBeenCalled()
+			expect(consoleInfoSpy).toHaveBeenCalledWith(`Failed connecting to Ollama at ${baseUrl}`)
+			expect(result).toEqual({})
+
+			consoleInfoSpy.mockRestore() // Restore original console.info
+		})
+
+		it("should handle models with null families field in API response", async () => {
+			const baseUrl = "http://localhost:11434"
+			const modelName = "test-model:latest"
+
+			const mockApiTagsResponse = {
+				models: [
+					{
+						name: modelName,
+						model: modelName,
+						modified_at: "2025-06-03T09:23:22.610222878-04:00",
+						size: 14333928010,
+						digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
+						details: {
+							family: "llama",
+							families: null, // This is the case we're testing
+							format: "gguf",
+							parameter_size: "23.6B",
+							parent_model: "",
+							quantization_level: "Q4_K_M",
+						},
+					},
+				],
+			}
+			const mockApiShowResponse = {
+				license: "Mock License",
+				modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
+				parameters: "num_ctx 4096\nstop_token <eos>",
+				template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
+				modified_at: "2025-06-03T09:23:22.610222878-04:00",
+				details: {
+					parent_model: "",
+					format: "gguf",
+					family: "llama",
+					families: null, // This is the case we're testing
+					parameter_size: "23.6B",
+					quantization_level: "Q4_K_M",
+				},
+				model_info: {
+					"ollama.context_length": 4096,
+					"some.other.info": "value",
+				},
+				capabilities: ["completion"],
+			}
+
+			mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
+			mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
+
+			const result = await getOllamaModels(baseUrl)
+
+			expect(mockedAxios.get).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
+
+			expect(mockedAxios.post).toHaveBeenCalledTimes(1)
+			expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
+
+			expect(typeof result).toBe("object")
+			expect(result).not.toBeInstanceOf(Array)
+			expect(Object.keys(result).length).toBe(1)
+			expect(result[modelName]).toBeDefined()
+
+			// Verify the model was parsed correctly despite null families
+			expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B")
+		})
+	})
+})

+ 70 - 0
src/api/providers/fetchers/lmstudio.ts

@@ -0,0 +1,70 @@
+import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types"
+import { LLM, LLMInfo, LLMInstanceInfo, LMStudioClient } from "@lmstudio/sdk"
+import axios from "axios"
+
+export const parseLMStudioModel = (rawModel: LLMInstanceInfo | LLMInfo): ModelInfo => {
+	// Handle both LLMInstanceInfo (from loaded models) and LLMInfo (from downloaded models)
+	const contextLength = "contextLength" in rawModel ? rawModel.contextLength : rawModel.maxContextLength
+
+	const modelInfo: ModelInfo = Object.assign({}, lMStudioDefaultModelInfo, {
+		description: `${rawModel.displayName} - ${rawModel.path}`,
+		contextWindow: contextLength,
+		supportsPromptCache: true,
+		supportsImages: rawModel.vision,
+		supportsComputerUse: false,
+		maxTokens: contextLength,
+	})
+
+	return modelInfo
+}
+
+export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Promise<Record<string, ModelInfo>> {
+	// clearing the input can leave an empty string; use the default in that case
+	baseUrl = baseUrl === "" ? "http://localhost:1234" : baseUrl
+
+	const models: Record<string, ModelInfo> = {}
+	// ws is required to connect using the LMStudio library
+	const lmsUrl = baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://")
+
+	try {
+		if (!URL.canParse(lmsUrl)) {
+			return models
+		}
+
+		// test the connection to LM Studio first
+		// errors will be caught further down
+		await axios.get(`${baseUrl}/v1/models`)
+
+		const client = new LMStudioClient({ baseUrl: lmsUrl })
+
+		// First, try to get all downloaded models
+		try {
+			const downloadedModels = await client.system.listDownloadedModels("llm")
+			for (const model of downloadedModels) {
+				// Use the model path as the key since that's what users select
+				models[model.path] = parseLMStudioModel(model)
+			}
+		} catch (error) {
+			console.warn("Failed to list downloaded models, falling back to loaded models only")
+
+			// Fall back to listing only loaded models
+			const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => {
+				return Promise.all(models.map((m) => m.getModelInfo()))
+			})) as Array<LLMInstanceInfo>
+
+			for (const lmstudioModel of loadedModels) {
+				models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel)
+			}
+		}
+	} catch (error) {
+		if (error.code === "ECONNREFUSED") {
+			console.warn(`Error connecting to LMStudio at ${baseUrl}`)
+		} else {
+			console.error(
+				`Error fetching LMStudio models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+			)
+		}
+	}
+
+	return models
+}

+ 9 - 0
src/api/providers/fetchers/modelCache.ts

@@ -15,6 +15,9 @@ import { getUnboundModels } from "./unbound"
 import { getLiteLLMModels } from "./litellm"
 import { GetModelsOptions } from "../../../shared/api"
 import { getKiloBaseUriFromToken } from "../../../utils/kilocode-token"
+import { getOllamaModels } from "./ollama"
+import { getLMStudioModels } from "./lmstudio"
+
 const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
 
 export /*kilocode_change*/ async function writeModels(router: RouterName, data: ModelRecord) {
@@ -90,6 +93,12 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 				models = cerebrasModels
 				break
 			// kilocode_change end
+			case "ollama":
+				models = await getOllamaModels(options.baseUrl)
+				break
+			case "lmstudio":
+				models = await getLMStudioModels(options.baseUrl)
+				break
 			default: {
 				// Ensures router is exhaustively checked if RouterName is a strict union
 				const exhaustiveCheck: never = provider

+ 100 - 0
src/api/providers/fetchers/ollama.ts

@@ -0,0 +1,100 @@
+import axios from "axios"
+import { ModelInfo, ollamaDefaultModelInfo } from "@roo-code/types"
+import { z } from "zod"
+
+const OllamaModelDetailsSchema = z.object({
+	family: z.string(),
+	families: z.array(z.string()).nullable().optional(),
+	format: z.string().optional(),
+	parameter_size: z.string(),
+	parent_model: z.string().optional(),
+	quantization_level: z.string().optional(),
+})
+
+const OllamaModelSchema = z.object({
+	details: OllamaModelDetailsSchema,
+	digest: z.string().optional(),
+	model: z.string(),
+	modified_at: z.string().optional(),
+	name: z.string(),
+	size: z.number().optional(),
+})
+
+const OllamaModelInfoResponseSchema = z.object({
+	modelfile: z.string().optional(),
+	parameters: z.string().optional(),
+	template: z.string().optional(),
+	details: OllamaModelDetailsSchema,
+	model_info: z.record(z.string(), z.any()),
+	capabilities: z.array(z.string()).optional(),
+})
+
+const OllamaModelsResponseSchema = z.object({
+	models: z.array(OllamaModelSchema),
+})
+
+type OllamaModelsResponse = z.infer<typeof OllamaModelsResponseSchema>
+
+type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema>
+
+export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo => {
+	const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
+	const contextWindow =
+		contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined
+
+	const modelInfo: ModelInfo = Object.assign({}, ollamaDefaultModelInfo, {
+		description: `Family: ${rawModel.details.family}, Context: ${contextWindow}, Size: ${rawModel.details.parameter_size}`,
+		contextWindow: contextWindow || ollamaDefaultModelInfo.contextWindow,
+		supportsPromptCache: true,
+		supportsImages: rawModel.capabilities?.includes("vision"),
+		supportsComputerUse: false,
+		maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow,
+	})
+
+	return modelInfo
+}
+
+export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promise<Record<string, ModelInfo>> {
+	const models: Record<string, ModelInfo> = {}
+
+	// clearing the input can leave an empty string; use the default in that case
+	baseUrl = baseUrl === "" ? "http://localhost:11434" : baseUrl
+
+	try {
+		if (!URL.canParse(baseUrl)) {
+			return models
+		}
+
+		const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`)
+		const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
+		let modelInfoPromises = []
+
+		if (parsedResponse.success) {
+			for (const ollamaModel of parsedResponse.data.models) {
+				modelInfoPromises.push(
+					axios
+						.post<OllamaModelInfoResponse>(`${baseUrl}/api/show`, {
+							model: ollamaModel.model,
+						})
+						.then((ollamaModelInfo) => {
+							models[ollamaModel.name] = parseOllamaModel(ollamaModelInfo.data)
+						}),
+				)
+			}
+
+			await Promise.all(modelInfoPromises)
+		} else {
+			console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`)
+		}
+	} catch (error) {
+		if (error.code === "ECONNREFUSED") {
+			console.warn(`Failed connecting to Ollama at ${baseUrl}`)
+		} else {
+			console.error(
+				`Error fetching Ollama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+			)
+		}
+	}
+
+	return models
+}

+ 1 - 0
src/api/providers/index.ts

@@ -3,6 +3,7 @@ export { AnthropicHandler } from "./anthropic"
 export { AwsBedrockHandler } from "./bedrock"
 export { CerebrasHandler } from "./cerebras" // kilocode_change
 export { ChutesHandler } from "./chutes"
+export { ClaudeCodeHandler } from "./claude-code"
 export { DeepSeekHandler } from "./deepseek"
 export { FakeAIHandler } from "./fake-ai"
 export { GeminiHandler } from "./gemini"

+ 0 - 15
src/api/providers/ollama.ts

@@ -1,6 +1,5 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import OpenAI from "openai"
-import axios from "axios"
 
 import { type ModelInfo, openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types"
 
@@ -111,17 +110,3 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl
 		}
 	}
 }
-
-export async function getOllamaModels(baseUrl = "http://localhost:11434") {
-	try {
-		if (!URL.canParse(baseUrl)) {
-			return []
-		}
-
-		const response = await axios.get(`${baseUrl}/api/tags`)
-		const modelsArray = response.data?.models?.map((model: any) => model.name) || []
-		return [...new Set<string>(modelsArray)]
-	} catch (error) {
-		return []
-	}
-}

+ 7 - 1
src/api/transform/stream.ts

@@ -1,6 +1,12 @@
 export type ApiStream = AsyncGenerator<ApiStreamChunk>
 
-export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk
+export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk | ApiStreamError
+
+export interface ApiStreamError {
+	type: "error"
+	error: string
+	message: string
+}
 
 export interface ApiStreamTextChunk {
 	type: "text"

+ 2 - 0
src/core/condense/index.ts

@@ -8,6 +8,8 @@ import { ApiMessage } from "../task-persistence/apiMessages"
 import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
 
 export const N_MESSAGES_TO_KEEP = 3
+export const MIN_CONDENSE_THRESHOLD = 5 // Minimum percentage of context window to trigger condensing
+export const MAX_CONDENSE_THRESHOLD = 100 // Maximum percentage of context window to trigger condensing
 
 const SUMMARY_PROMPT = `\
 Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.

+ 1 - 1
src/core/diff/strategies/multi-file-search-replace.ts

@@ -485,7 +485,7 @@ Each file requires its own path, start_line, and diff elements.
 
 		const replacements = matches
 			.map((match) => ({
-				startLine: Number(match[2] ?? 0),
+				startLine: _paramStartLine ?? Number(match[2] ?? 0),
 				searchContent: match[6],
 				replaceContent: match[7],
 			}))

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap

@@ -298,25 +298,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -449,7 +446,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap

@@ -195,25 +195,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -346,7 +343,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap

@@ -369,25 +369,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -533,7 +530,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap

@@ -369,25 +369,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -539,7 +536,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap

@@ -325,25 +325,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -476,7 +473,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap

@@ -320,25 +320,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -471,7 +468,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap

@@ -373,25 +373,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -527,7 +524,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap

@@ -320,25 +320,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -471,7 +468,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap

@@ -408,25 +408,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -559,7 +556,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap

@@ -320,25 +320,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -471,7 +468,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap

@@ -373,25 +373,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -527,7 +524,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap

@@ -369,25 +369,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -539,7 +536,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 3 - 6
src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap

@@ -320,25 +320,22 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
 </ask_followup_question>
 
 ## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
 - result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
 </result>
-<command>Command to demonstrate result (optional)</command>
 </attempt_completion>
 
-Example: Requesting to attempt completion with a result and command
+Example: Requesting to attempt completion with a result
 <attempt_completion>
 <result>
 I've updated the CSS
 </result>
-<command>open index.html</command>
 </attempt_completion>
 
 ## switch_mode
@@ -471,7 +468,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. `open index.html` to show the website you've built.
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
 
 

+ 2 - 1
src/core/prompts/instructions/create-mcp-server.ts

@@ -50,6 +50,7 @@ Common configuration options for both types:
 - \`disabled\`: (optional) Set to true to temporarily disable the server
 - \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
 - \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
+- \`disabledTools\`: (optional) Array of tool names that are not included in the system prompt and won't be used
 
 ### Example Local MCP Server
 
@@ -276,7 +277,7 @@ npm run build
 
 5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
 
-IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and alwaysAllow=[].
+IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[].
 
 \`\`\`json
 {

+ 1 - 0
src/core/prompts/sections/mcp-servers.ts

@@ -17,6 +17,7 @@ export async function getMcpServersSection(
 					.filter((server) => server.status === "connected")
 					.map((server) => {
 						const tools = server.tools
+							?.filter((tool) => tool.enabledForPrompt !== false)
 							?.map((tool) => {
 								const schemaStr = tool.inputSchema
 									? `    Input Schema:

+ 1 - 9
src/core/prompts/sections/objective.ts

@@ -1,4 +1,3 @@
-import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
 import { CodeIndexManager } from "../../../services/code-index/manager"
 
 export function getObjectiveSection(
@@ -15,13 +14,6 @@ export function getObjectiveSection(
 		? "First, if the task involves understanding existing code or functionality, you MUST use the `codebase_search` tool to search for relevant code based on the task's intent BEFORE using any other search or file exploration tools. Then, "
 		: "First, "
 
-	// Check if command execution is disabled via experiment
-	const isCommandDisabled = experimentsConfig && experimentsConfig[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]
-
-	const commandInstruction = !isCommandDisabled
-		? " You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built."
-		: ""
-
 	return `====
 
 OBJECTIVE
@@ -31,6 +23,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. ${codebaseSearchInstruction}analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.${commandInstruction}
+4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
 }

+ 53 - 113
src/core/prompts/tools/__tests__/attempt-completion.spec.ts

@@ -1,129 +1,69 @@
 import { getAttemptCompletionDescription } from "../attempt-completion"
-import { EXPERIMENT_IDS } from "../../../../shared/experiments"
 
-describe("getAttemptCompletionDescription - DISABLE_COMPLETION_COMMAND experiment", () => {
-	describe("when experiment is disabled (default)", () => {
-		it("should include command parameter in the description", () => {
-			const args = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: false,
-				},
-			}
-
-			const description = getAttemptCompletionDescription(args)
-
-			// Check that command parameter is included
-			expect(description).toContain("- command: (optional)")
-			expect(description).toContain("A CLI command to execute to show a live demo")
-			expect(description).toContain("<command>Command to demonstrate result (optional)</command>")
-			expect(description).toContain("<command>open index.html</command>")
-		})
-
-		it("should include command parameter when experiments is undefined", () => {
-			const args = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-			}
-
-			const description = getAttemptCompletionDescription(args)
-
-			// Check that command parameter is included
-			expect(description).toContain("- command: (optional)")
-			expect(description).toContain("A CLI command to execute to show a live demo")
-			expect(description).toContain("<command>Command to demonstrate result (optional)</command>")
-			expect(description).toContain("<command>open index.html</command>")
-		})
-
-		it("should include command parameter when no args provided", () => {
-			const description = getAttemptCompletionDescription()
-
-			// Check that command parameter is included
-			expect(description).toContain("- command: (optional)")
-			expect(description).toContain("A CLI command to execute to show a live demo")
-			expect(description).toContain("<command>Command to demonstrate result (optional)</command>")
-			expect(description).toContain("<command>open index.html</command>")
-		})
+describe("getAttemptCompletionDescription", () => {
+	it("should NOT include command parameter in the description", () => {
+		const args = {
+			cwd: "/test/path",
+			supportsComputerUse: false,
+		}
+
+		const description = getAttemptCompletionDescription(args)
+
+		// Check that command parameter is NOT included (permanently disabled)
+		expect(description).not.toContain("- command: (optional)")
+		expect(description).not.toContain("A CLI command to execute to show a live demo")
+		expect(description).not.toContain("<command>Command to demonstrate result (optional)</command>")
+		expect(description).not.toContain("<command>open index.html</command>")
+
+		// But should still have the basic structure
+		expect(description).toContain("## attempt_completion")
+		expect(description).toContain("- result: (required)")
+		expect(description).toContain("<attempt_completion>")
+		expect(description).toContain("</attempt_completion>")
 	})
 
-	describe("when experiment is enabled", () => {
-		it("should NOT include command parameter in the description", () => {
-			const args = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: true,
-				},
-			}
-
-			const description = getAttemptCompletionDescription(args)
+	it("should work when no args provided", () => {
+		const description = getAttemptCompletionDescription()
 
-			// Check that command parameter is NOT included
-			expect(description).not.toContain("- command: (optional)")
-			expect(description).not.toContain("A CLI command to execute to show a live demo")
-			expect(description).not.toContain("<command>Command to demonstrate result (optional)</command>")
-			expect(description).not.toContain("<command>open index.html</command>")
+		// Check that command parameter is NOT included (permanently disabled)
+		expect(description).not.toContain("- command: (optional)")
+		expect(description).not.toContain("A CLI command to execute to show a live demo")
+		expect(description).not.toContain("<command>Command to demonstrate result (optional)</command>")
+		expect(description).not.toContain("<command>open index.html</command>")
 
-			// But should still have the basic structure
-			expect(description).toContain("## attempt_completion")
-			expect(description).toContain("- result: (required)")
-			expect(description).toContain("<attempt_completion>")
-			expect(description).toContain("</attempt_completion>")
-		})
+		// But should still have the basic structure
+		expect(description).toContain("## attempt_completion")
+		expect(description).toContain("- result: (required)")
+		expect(description).toContain("<attempt_completion>")
+		expect(description).toContain("</attempt_completion>")
+	})
 
-		it("should show example without command", () => {
-			const args = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: true,
-				},
-			}
+	it("should show example without command", () => {
+		const args = {
+			cwd: "/test/path",
+			supportsComputerUse: false,
+		}
 
-			const description = getAttemptCompletionDescription(args)
+		const description = getAttemptCompletionDescription(args)
 
-			// Check example format
-			expect(description).toContain("Example: Requesting to attempt completion with a result")
-			expect(description).toContain("I've updated the CSS")
-			expect(description).not.toContain("Example: Requesting to attempt completion with a result and command")
-		})
+		// Check example format
+		expect(description).toContain("Example: Requesting to attempt completion with a result")
+		expect(description).toContain("I've updated the CSS")
+		expect(description).not.toContain("Example: Requesting to attempt completion with a result and command")
 	})
 
-	describe("description content", () => {
-		it("should maintain core functionality description regardless of experiment", () => {
-			const argsWithExperimentDisabled = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: false,
-				},
-			}
-
-			const argsWithExperimentEnabled = {
-				cwd: "/test/path",
-				supportsComputerUse: false,
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: true,
-				},
-			}
-
-			const descriptionDisabled = getAttemptCompletionDescription(argsWithExperimentDisabled)
-			const descriptionEnabled = getAttemptCompletionDescription(argsWithExperimentEnabled)
+	it("should contain core functionality description", () => {
+		const description = getAttemptCompletionDescription()
 
-			// Both should contain core functionality
-			const coreText = "After each tool use, the user will respond with the result of that tool use"
-			expect(descriptionDisabled).toContain(coreText)
-			expect(descriptionEnabled).toContain(coreText)
+		// Should contain core functionality
+		const coreText = "After each tool use, the user will respond with the result of that tool use"
+		expect(description).toContain(coreText)
 
-			// Both should contain the important note
-			const importantNote = "IMPORTANT NOTE: This tool CANNOT be used until you've confirmed"
-			expect(descriptionDisabled).toContain(importantNote)
-			expect(descriptionEnabled).toContain(importantNote)
+		// Should contain the important note
+		const importantNote = "IMPORTANT NOTE: This tool CANNOT be used until you've confirmed"
+		expect(description).toContain(importantNote)
 
-			// Both should contain result parameter
-			expect(descriptionDisabled).toContain("- result: (required)")
-			expect(descriptionEnabled).toContain("- result: (required)")
-		})
+		// Should contain result parameter
+		expect(description).toContain("- result: (required)")
 	})
 })

+ 4 - 30
src/core/prompts/tools/attempt-completion.ts

@@ -1,41 +1,17 @@
-import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
 import { ToolArgs } from "./types"
 
 export function getAttemptCompletionDescription(args?: ToolArgs): string {
-	// Check if command execution is disabled via experiment
-	const isCommandDisabled =
-		args?.experiments && experiments.isEnabled(args.experiments, EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND)
-
-	const baseDescription = `## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user.${!isCommandDisabled ? " Optionally you may provide a CLI command to showcase the result of your work." : ""} The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
+	return `## attempt_completion
+Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
 IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
 Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.`
-
-	const commandParameter = !isCommandDisabled
-		? `
-- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use \`open index.html\` to display a created html website, or \`open localhost:3000\` to display a locally running development server. But DO NOT use commands like \`echo\` or \`cat\` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.`
-		: ""
-
-	const usage = `
+- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
 Usage:
 <attempt_completion>
 <result>
 Your final result description here
-</result>${!isCommandDisabled ? "\n<command>Command to demonstrate result (optional)</command>" : ""}
-</attempt_completion>`
-
-	const example = !isCommandDisabled
-		? `
-
-Example: Requesting to attempt completion with a result and command
-<attempt_completion>
-<result>
-I've updated the CSS
 </result>
-<command>open index.html</command>
-</attempt_completion>`
-		: `
+</attempt_completion>
 
 Example: Requesting to attempt completion with a result
 <attempt_completion>
@@ -43,6 +19,4 @@ Example: Requesting to attempt completion with a result
 I've updated the CSS
 </result>
 </attempt_completion>`
-
-	return baseDescription + commandParameter + usage + example
 }

+ 260 - 1
src/core/sliding-window/__tests__/sliding-window.spec.ts

@@ -19,7 +19,14 @@ import {
 // Create a mock ApiHandler for testing
 class MockApiHandler extends BaseProvider {
 	createMessage(): any {
-		throw new Error("Method not implemented.")
+		// Mock implementation for testing - returns an async iterable stream
+		const mockStream = {
+			async *[Symbol.asyncIterator]() {
+				yield { type: "text", text: "Mock summary content" }
+				yield { type: "usage", inputTokens: 100, outputTokens: 50 }
+			},
+		}
+		return mockStream
 	}
 
 	getModel(): { id: string; info: ModelInfo } {
@@ -265,6 +272,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Check the new return type
@@ -304,6 +313,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			expect(result).toEqual({
@@ -337,6 +348,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			const result2 = await truncateConversationIfNeeded({
@@ -349,6 +362,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			expect(result1.messages).toEqual(result2.messages)
@@ -368,6 +383,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			const result4 = await truncateConversationIfNeeded({
@@ -380,6 +397,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			expect(result3.messages).toEqual(result4.messages)
@@ -414,6 +433,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(resultWithSmall).toEqual({
 				messages: messagesWithSmallContent,
@@ -447,6 +468,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(resultWithLarge.messages).not.toEqual(messagesWithLargeContent) // Should truncate
 			expect(resultWithLarge.summary).toBe("")
@@ -473,6 +496,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(resultWithVeryLarge.messages).not.toEqual(messagesWithVeryLargeContent) // Should truncate
 			expect(resultWithVeryLarge.summary).toBe("")
@@ -509,6 +534,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result).toEqual({
 				messages: expectedResult,
@@ -554,6 +581,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Verify summarizeConversation was called with the right parameters
@@ -619,6 +648,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Verify summarizeConversation was called
@@ -664,6 +695,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 50, // This shouldn't matter since autoCondenseContext is false
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Verify summarizeConversation was not called
@@ -719,6 +752,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 50, // Set threshold to 50% - our tokens are at 60%
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Verify summarizeConversation was called with the right parameters
@@ -769,6 +804,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 50, // Set threshold to 50% - our tokens are at 40%
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 
 			// Verify summarizeConversation was not called
@@ -787,6 +824,212 @@ describe("Sliding Window", () => {
 		})
 	})
 
+	/**
+	 * Tests for profile-specific thresholds functionality
+	 */
+	describe("profile-specific thresholds", () => {
+		const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({
+			contextWindow,
+			supportsPromptCache: true,
+			maxTokens,
+		})
+
+		const messages: ApiMessage[] = [
+			{ role: "user", content: "First message" },
+			{ role: "assistant", content: "Second message" },
+			{ role: "user", content: "Third message" },
+			{ role: "assistant", content: "Fourth message" },
+			{ role: "user", content: "Fifth message" },
+		]
+
+		/**
+		 * Test that a profile's specific threshold is correctly used instead of the global threshold
+		 * when defined in profileThresholds
+		 */
+		it("should use profile-specific threshold when enabled and profile has specific threshold", async () => {
+			const modelInfo = createModelInfo(100000, 30000)
+			const profileThresholds = {
+				"test-profile": 60, // Profile-specific threshold of 60%
+			}
+			const currentProfileId = "test-profile"
+			const contextWindow = modelInfo.contextWindow
+
+			// Set tokens to 65% of context window - above profile threshold (60%) but below global default (100%)
+			const totalTokens = Math.floor(contextWindow * 0.65) // 65000 tokens
+
+			// Create messages with very small content in the last one to avoid token overflow
+			const messagesWithSmallContent = [
+				...messages.slice(0, -1),
+				{ ...messages[messages.length - 1], content: "" },
+			]
+
+			// Mock the summarizeConversation function
+			const mockSummary = "Profile-specific threshold summary"
+			const mockCost = 0.03
+			const mockSummarizeResponse: condenseModule.SummarizeResponse = {
+				messages: [
+					{ role: "user", content: "First message" },
+					{ role: "assistant", content: mockSummary, isSummary: true },
+					{ role: "user", content: "Last message" },
+				],
+				summary: mockSummary,
+				cost: mockCost,
+				newContextTokens: 100,
+			}
+
+			const summarizeSpy = vi
+				.spyOn(condenseModule, "summarizeConversation")
+				.mockResolvedValue(mockSummarizeResponse)
+
+			const result = await truncateConversationIfNeeded({
+				messages: messagesWithSmallContent,
+				totalTokens,
+				contextWindow,
+				maxTokens: modelInfo.maxTokens,
+				apiHandler: mockApiHandler,
+				autoCondenseContext: true,
+				autoCondenseContextPercent: 100, // Global threshold of 100%
+				systemPrompt: "System prompt",
+				taskId,
+				profileThresholds,
+				currentProfileId,
+			})
+
+			// Should use summarization because 65% > 60% (profile threshold)
+			expect(summarizeSpy).toHaveBeenCalled()
+			expect(result).toMatchObject({
+				messages: mockSummarizeResponse.messages,
+				summary: mockSummary,
+				cost: mockCost,
+				prevContextTokens: totalTokens,
+			})
+
+			// Clean up
+			summarizeSpy.mockRestore()
+		})
+
+		/**
+		 * Test that when a profile's threshold is set to -1,
+		 * the function correctly falls back to using the global autoCondenseContextPercent
+		 */
+		it("should fall back to global threshold when profile threshold is -1", async () => {
+			const modelInfo = createModelInfo(100000, 30000)
+			const profileThresholds = {
+				"test-profile": -1, // Profile threshold set to -1 (use global)
+			}
+			const currentProfileId = "test-profile"
+			const contextWindow = modelInfo.contextWindow
+
+			// Set tokens to 80% of context window - above global threshold (75%) but would be below if profile had its own
+			const totalTokens = Math.floor(contextWindow * 0.8) // 80000 tokens
+
+			// Create messages with very small content in the last one to avoid token overflow
+			const messagesWithSmallContent = [
+				...messages.slice(0, -1),
+				{ ...messages[messages.length - 1], content: "" },
+			]
+
+			// Mock the summarizeConversation function
+			const mockSummary = "Global threshold fallback summary"
+			const mockCost = 0.04
+			const mockSummarizeResponse: condenseModule.SummarizeResponse = {
+				messages: [
+					{ role: "user", content: "First message" },
+					{ role: "assistant", content: mockSummary, isSummary: true },
+					{ role: "user", content: "Last message" },
+				],
+				summary: mockSummary,
+				cost: mockCost,
+				newContextTokens: 120,
+			}
+
+			const summarizeSpy = vi
+				.spyOn(condenseModule, "summarizeConversation")
+				.mockResolvedValue(mockSummarizeResponse)
+
+			const result = await truncateConversationIfNeeded({
+				messages: messagesWithSmallContent,
+				totalTokens,
+				contextWindow,
+				maxTokens: modelInfo.maxTokens,
+				apiHandler: mockApiHandler,
+				autoCondenseContext: true,
+				autoCondenseContextPercent: 75, // Global threshold of 75%
+				systemPrompt: "System prompt",
+				taskId,
+				profileThresholds,
+				currentProfileId,
+			})
+
+			// Should use summarization because 80% > 75% (global threshold, since profile is -1)
+			expect(summarizeSpy).toHaveBeenCalled()
+			expect(result).toMatchObject({
+				messages: mockSummarizeResponse.messages,
+				summary: mockSummary,
+				cost: mockCost,
+				prevContextTokens: totalTokens,
+			})
+
+			// Clean up
+			summarizeSpy.mockRestore()
+		})
+
+		/**
+		 * Test that when a profile does not have a specific threshold defined,
+		 * the function correctly falls back to the global default
+		 */
+		it("should fall back to global threshold when profile has no specific threshold", async () => {
+			const modelInfo = createModelInfo(100000, 30000)
+			const profileThresholds = {
+				"other-profile": 50, // Different profile has a threshold
+			}
+			const currentProfileId = "test-profile" // This profile is not in profileThresholds
+			const contextWindow = modelInfo.contextWindow
+
+			// Calculate allowedTokens: contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens
+			// allowedTokens = 100000 * 0.9 - 30000 = 60000
+			// Set tokens to be below both the global threshold (80%) and allowedTokens
+			const totalTokens = 50000 // 50% of context window, well below 60000 allowedTokens and 80% threshold
+
+			// Create messages with very small content in the last one to avoid token overflow
+			const messagesWithSmallContent = [
+				...messages.slice(0, -1),
+				{ ...messages[messages.length - 1], content: "" },
+			]
+
+			// Reset any previous mock calls
+			vi.clearAllMocks()
+			const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation")
+
+			const result = await truncateConversationIfNeeded({
+				messages: messagesWithSmallContent,
+				totalTokens,
+				contextWindow,
+				maxTokens: modelInfo.maxTokens,
+				apiHandler: mockApiHandler,
+				autoCondenseContext: true,
+				autoCondenseContextPercent: 80, // Global threshold of 80%
+				systemPrompt: "System prompt",
+				taskId,
+				profileThresholds,
+				currentProfileId,
+			})
+
+			// Should NOT use summarization because 50% < 80% (global threshold, since profile has no specific threshold)
+			// and totalTokens (50000) < allowedTokens (60000)
+			expect(summarizeSpy).not.toHaveBeenCalled()
+			expect(result).toEqual({
+				messages: messagesWithSmallContent,
+				summary: "",
+				cost: 0,
+				prevContextTokens: totalTokens,
+			})
+
+			// Clean up
+			summarizeSpy.mockRestore()
+		})
+	})
+
 	/**
 	 * Tests for the getMaxTokens function (private but tested through truncateConversationIfNeeded)
 	 */
@@ -829,6 +1072,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result1).toEqual({
 				messages: messagesWithSmallContent,
@@ -848,6 +1093,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result2.messages).not.toEqual(messagesWithSmallContent)
 			expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@@ -878,6 +1125,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result1).toEqual({
 				messages: messagesWithSmallContent,
@@ -897,6 +1146,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result2.messages).not.toEqual(messagesWithSmallContent)
 			expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@@ -926,6 +1177,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result1.messages).toEqual(messagesWithSmallContent)
 
@@ -940,6 +1193,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result2).not.toEqual(messagesWithSmallContent)
 			expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@@ -967,6 +1222,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result1.messages).toEqual(messagesWithSmallContent)
 
@@ -981,6 +1238,8 @@ describe("Sliding Window", () => {
 				autoCondenseContextPercent: 100,
 				systemPrompt: "System prompt",
 				taskId,
+				profileThresholds: {},
+				currentProfileId: "default",
 			})
 			expect(result2).not.toEqual(messagesWithSmallContent)
 			expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction

+ 26 - 2
src/core/sliding-window/index.ts

@@ -3,7 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
 import { TelemetryService } from "@roo-code/telemetry"
 
 import { ApiHandler } from "../../api"
-import { summarizeConversation, SummarizeResponse } from "../condense"
+import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
 import { ApiMessage } from "../task-persistence/apiMessages"
 
 /**
@@ -74,6 +74,8 @@ type TruncateOptions = {
 	taskId: string
 	customCondensingPrompt?: string
 	condensingApiHandler?: ApiHandler
+	profileThresholds: Record<string, number>
+	currentProfileId: string
 }
 
 type TruncateResponse = SummarizeResponse & { prevContextTokens: number }
@@ -97,6 +99,8 @@ export async function truncateConversationIfNeeded({
 	taskId,
 	customCondensingPrompt,
 	condensingApiHandler,
+	profileThresholds,
+	currentProfileId,
 }: TruncateOptions): Promise<TruncateResponse> {
 	let error: string | undefined
 	let cost = 0
@@ -117,9 +121,29 @@ export async function truncateConversationIfNeeded({
 	// Truncate if we're within TOKEN_BUFFER_PERCENTAGE of the context window
 	const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens
 
+	// Determine the effective threshold to use
+	let effectiveThreshold = autoCondenseContextPercent
+	const profileThreshold = profileThresholds[currentProfileId]
+	if (profileThreshold !== undefined) {
+		if (profileThreshold === -1) {
+			// Special case: -1 means inherit from global setting
+			effectiveThreshold = autoCondenseContextPercent
+		} else if (profileThreshold >= MIN_CONDENSE_THRESHOLD && profileThreshold <= MAX_CONDENSE_THRESHOLD) {
+			// Valid custom threshold
+			effectiveThreshold = profileThreshold
+		} else {
+			// Invalid threshold value, fall back to global setting
+			console.warn(
+				`Invalid profile threshold ${profileThreshold} for profile "${currentProfileId}". Using global default of ${autoCondenseContextPercent}%`,
+			)
+			effectiveThreshold = autoCondenseContextPercent
+		}
+	}
+	// If no specific threshold is found for the profile, fall back to global setting
+
 	if (autoCondenseContext) {
 		const contextPercent = (100 * prevContextTokens) / contextWindow
-		if (contextPercent >= autoCondenseContextPercent || prevContextTokens > allowedTokens) {
+		if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
 			// Attempt to intelligently condense the context
 			const result = await summarizeConversation(
 				messages,

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

@@ -1740,6 +1740,7 @@ export class Task extends EventEmitter<ClineEvents> {
 			mode,
 			autoCondenseContext = true,
 			autoCondenseContextPercent = 100,
+			profileThresholds = {},
 		} = state ?? {}
 
 		// Get condensing configuration for automatic triggers
@@ -1816,6 +1817,8 @@ export class Task extends EventEmitter<ClineEvents> {
 				taskId: this.taskId,
 				customCondensingPrompt,
 				condensingApiHandler,
+				profileThresholds,
+				currentProfileId: state?.currentApiConfigName || "default",
 			})
 			if (truncateResult.messages !== this.apiConversationHistory) {
 				await this.overwriteApiConversationHistory(truncateResult.messages)

+ 0 - 356
src/core/tools/__tests__/attemptCompletionTool.experiment.spec.ts

@@ -1,356 +0,0 @@
-// Mocks must come first, before imports
-vi.mock("../executeCommandTool", () => ({
-	executeCommand: vi.fn(),
-}))
-
-vi.mock("@roo-code/telemetry", () => ({
-	TelemetryService: {
-		instance: {
-			captureTaskCompleted: vi.fn(),
-		},
-	},
-}))
-
-// Then imports
-import type { Mock } from "vitest"
-import { attemptCompletionTool } from "../attemptCompletionTool"
-import { EXPERIMENT_IDS } from "../../../shared/experiments"
-import { executeCommand } from "../executeCommandTool"
-
-describe("attemptCompletionTool - DISABLE_COMPLETION_COMMAND experiment", () => {
-	let mockCline: any
-	let mockAskApproval: Mock
-	let mockHandleError: Mock
-	let mockPushToolResult: Mock
-	let mockRemoveClosingTag: Mock
-	let mockToolDescription: Mock
-	let mockAskFinishSubTaskApproval: Mock
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-
-		mockAskApproval = vi.fn().mockResolvedValue(true)
-		mockHandleError = vi.fn()
-		mockPushToolResult = vi.fn()
-		mockRemoveClosingTag = vi.fn((tag, content) => content)
-		mockToolDescription = vi.fn().mockReturnValue("attempt_completion")
-		mockAskFinishSubTaskApproval = vi.fn()
-
-		mockCline = {
-			say: vi.fn(),
-			ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked", text: "", images: [] }),
-			clineMessages: [],
-			lastMessageTs: Date.now(),
-			consecutiveMistakeCount: 0,
-			sayAndCreateMissingParamError: vi.fn(),
-			recordToolError: vi.fn(),
-			emit: vi.fn(),
-			getTokenUsage: vi.fn().mockReturnValue({}),
-			toolUsage: {},
-			userMessageContent: [],
-			taskId: "test-task-id",
-			providerRef: {
-				deref: vi.fn().mockReturnValue({
-					getState: vi.fn().mockResolvedValue({
-						experiments: {},
-					}),
-				}),
-			},
-		}
-	})
-
-	describe("when experiment is disabled (default)", () => {
-		beforeEach(() => {
-			mockCline.providerRef.deref().getState.mockResolvedValue({
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: false,
-				},
-			})
-		})
-
-		it("should execute command when provided", async () => {
-			const mockExecuteCommand = executeCommand as Mock
-			mockExecuteCommand.mockResolvedValue([false, "Command executed successfully"])
-
-			// Mock clineMessages with a previous message that's not a command ask
-			mockCline.clineMessages = [{ say: "previous_message", text: "Previous message" }]
-
-			const block = {
-				params: {
-					result: "Task completed successfully",
-					command: "npm test",
-				},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			// When there's a lastMessage that's not a command ask, it should say completion_result first
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockCline.emit).toHaveBeenCalledWith(
-				"taskCompleted",
-				mockCline.taskId,
-				expect.any(Object),
-				expect.any(Object),
-			)
-			expect(mockAskApproval).toHaveBeenCalledWith("command", "npm test")
-			expect(mockExecuteCommand).toHaveBeenCalled()
-		})
-
-		it("should not execute command when user rejects", async () => {
-			mockAskApproval.mockResolvedValue(false)
-			const mockExecuteCommand = executeCommand as Mock
-
-			// Mock clineMessages with a previous message that's not a command ask
-			mockCline.clineMessages = [{ say: "previous_message", text: "Previous message" }]
-
-			const block = {
-				params: {
-					result: "Task completed successfully",
-					command: "npm test",
-				},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			// Should say completion_result and emit before asking for approval
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockCline.emit).toHaveBeenCalledWith(
-				"taskCompleted",
-				mockCline.taskId,
-				expect.any(Object),
-				expect.any(Object),
-			)
-			expect(mockAskApproval).toHaveBeenCalledWith("command", "npm test")
-			expect(mockExecuteCommand).not.toHaveBeenCalled()
-		})
-	})
-
-	describe("when experiment is enabled", () => {
-		beforeEach(() => {
-			mockCline.providerRef.deref().getState.mockResolvedValue({
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: true,
-				},
-			})
-		})
-
-		it("should NOT execute command even when provided", async () => {
-			const mockExecuteCommand = executeCommand as Mock
-
-			const block = {
-				params: {
-					result: "Task completed successfully",
-					command: "npm test",
-				},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockAskApproval).not.toHaveBeenCalled()
-			expect(mockExecuteCommand).not.toHaveBeenCalled()
-		})
-
-		it("should complete normally without command execution", async () => {
-			const block = {
-				params: {
-					result: "Task completed successfully",
-					command: "npm test",
-				},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockCline.emit).toHaveBeenCalledWith(
-				"taskCompleted",
-				mockCline.taskId,
-				expect.any(Object),
-				expect.any(Object),
-			)
-			expect(mockAskApproval).not.toHaveBeenCalled()
-		})
-	})
-
-	describe("when no command is provided", () => {
-		it("should work the same regardless of experiment state", async () => {
-			const block = {
-				params: {
-					result: "Task completed successfully",
-				},
-				partial: false,
-			}
-
-			// Test with experiment disabled
-			mockCline.providerRef.deref().getState.mockResolvedValue({
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: false,
-				},
-			})
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockAskApproval).not.toHaveBeenCalled()
-
-			// Reset mocks
-			vi.clearAllMocks()
-
-			// Test with experiment enabled
-			mockCline.providerRef.deref().getState.mockResolvedValue({
-				experiments: {
-					[EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND]: true,
-				},
-			})
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			expect(mockCline.say).toHaveBeenCalledWith(
-				"completion_result",
-				"Task completed successfully",
-				undefined,
-				false,
-			)
-			expect(mockAskApproval).not.toHaveBeenCalled()
-		})
-	})
-
-	describe("error handling", () => {
-		it("should handle missing result parameter", async () => {
-			const block = {
-				params: {},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			expect(mockCline.consecutiveMistakeCount).toBe(1)
-			expect(mockCline.recordToolError).toHaveBeenCalledWith("attempt_completion")
-			expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("attempt_completion", "result")
-		})
-
-		it("should handle state retrieval errors gracefully", async () => {
-			// Mock provider ref to return null
-			mockCline.providerRef.deref.mockReturnValue(null)
-
-			// Mock clineMessages to simulate no previous messages
-			mockCline.clineMessages = []
-
-			const block = {
-				params: {
-					result: "Task completed successfully",
-					command: "npm test",
-				},
-				partial: false,
-			}
-
-			await attemptCompletionTool(
-				mockCline,
-				block as any,
-				mockAskApproval,
-				mockHandleError,
-				mockPushToolResult,
-				mockRemoveClosingTag,
-				mockToolDescription,
-				mockAskFinishSubTaskApproval,
-			)
-
-			// When state retrieval fails, it defaults to not disabled (false), so it will try to execute command
-			// Since there's no lastMessage, it goes directly to askApproval
-			expect(mockAskApproval).toHaveBeenCalledWith("command", "npm test")
-			expect(mockCline.say).not.toHaveBeenCalled()
-		})
-	})
-})

+ 2 - 2
src/core/tools/__tests__/writeToFileTool.spec.ts

@@ -243,14 +243,14 @@ describe("writeToFileTool", () => {
 	})
 
 	describe("file existence detection", () => {
-		it("detects existing file and sets editType to modify", async () => {
+		it.skipIf(process.platform === "win32")("detects existing file and sets editType to modify", async () => {
 			await executeWriteFileTool({}, { fileExists: true })
 
 			expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath)
 			expect(mockCline.diffViewProvider.editType).toBe("modify")
 		})
 
-		it("detects new file and sets editType to create", async () => {
+		it.skipIf(process.platform === "win32")("detects new file and sets editType to create", async () => {
 			await executeWriteFileTool({}, { fileExists: false })
 
 			expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath)

+ 5 - 52
src/core/tools/attemptCompletionTool.ts

@@ -14,8 +14,6 @@ import {
 	AskFinishSubTaskApproval,
 } from "../../shared/tools"
 import { formatResponse } from "../prompts/responses"
-import { type ExecuteCommandOptions, executeCommand } from "./executeCommandTool"
-import { EXPERIMENT_IDS, experiments, experimentDefault } from "../../shared/experiments"
 
 export async function attemptCompletionTool(
 	cline: Task,
@@ -67,48 +65,11 @@ export async function attemptCompletionTool(
 
 			cline.consecutiveMistakeCount = 0
 
-			let commandResult: ToolResponse | undefined
-
-			// Check if command execution is disabled via experiment
-			const state = await cline.providerRef.deref()?.getState()
-			const experimentsConfig = state?.experiments ?? experimentDefault
-			const isCommandDisabled = experiments.isEnabled(
-				experimentsConfig,
-				EXPERIMENT_IDS.DISABLE_COMPLETION_COMMAND,
-			)
-
-			if (command && !isCommandDisabled) {
-				if (lastMessage && lastMessage.ask !== "command") {
-					// Haven't sent a command message yet so first send completion_result then command.
-					await cline.say("completion_result", result, undefined, false)
-					TelemetryService.instance.captureTaskCompleted(cline.taskId)
-					cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
-				}
-
-				// Complete command message.
-				const didApprove = await askApproval("command", command)
-
-				if (!didApprove) {
-					return
-				}
-
-				const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
-				const options: ExecuteCommandOptions = { executionId, command }
-				const [userRejected, execCommandResult] = await executeCommand(cline, options)
-
-				if (userRejected) {
-					cline.didRejectTool = true
-					pushToolResult(execCommandResult)
-					return
-				}
-
-				// User didn't reject, but the command may have output.
-				commandResult = execCommandResult
-			} else {
-				await cline.say("completion_result", result, undefined, false)
-				TelemetryService.instance.captureTaskCompleted(cline.taskId)
-				cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
-			}
+			// Command execution is permanently disabled in attempt_completion
+			// Users must use execute_command tool separately before attempt_completion
+			await cline.say("completion_result", result, undefined, false)
+			TelemetryService.instance.captureTaskCompleted(cline.taskId)
+			cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
 
 			if (cline.parentTask) {
 				const didApprove = await askFinishSubTaskApproval()
@@ -138,14 +99,6 @@ export async function attemptCompletionTool(
 			await cline.say("user_feedback", text ?? "", images)
 			const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
 
-			if (commandResult) {
-				if (typeof commandResult === "string") {
-					toolResults.push({ type: "text", text: commandResult })
-				} else if (Array.isArray(commandResult)) {
-					toolResults.push(...commandResult)
-				}
-			}
-
 			toolResults.push({
 				type: "text",
 				text: `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,

+ 53 - 12
src/core/webview/ClineProvider.ts

@@ -23,6 +23,7 @@ import {
 	type TerminalActionPromptType,
 	type HistoryItem,
 	type CloudUserInfo,
+	type MarketplaceItem,
 	requestyDefaultModelId,
 	openRouterDefaultModelId,
 	glamaDefaultModelId,
@@ -37,7 +38,7 @@ import { Package } from "../../shared/package"
 import { findLast } from "../../shared/array"
 import { supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
-import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
 import { Mode, defaultModeSlug } from "../../shared/modes"
 import { experimentDefault, experiments, EXPERIMENT_IDS } from "../../shared/experiments"
 import { formatLanguage } from "../../shared/language"
@@ -229,6 +230,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 		await this.getCurrentCline()?.resumePausedTask(lastMessage)
 	}
 
+	// Clear the current task without treating it as a subtask
+	// This is used when the user cancels a task that is not a subtask
+	async clearTask() {
+		await this.removeClineFromStack()
+	}
+
 	/*
 	VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
 	- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -661,7 +668,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 
 		const csp = [
 			"default-src 'none'",
-			`font-src ${webview.cspSource}`,
+			`font-src ${webview.cspSource} data:`,
 			`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
 			`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data: https://*.googleusercontent.com https://*.googleapis.com`, // kilocode_change: add https://*.googleusercontent.com and https://*.googleapis.com
 			`media-src ${webview.cspSource}`,
@@ -749,7 +756,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
             <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
             <meta name="theme-color" content="#000000">
 			<!-- kilocode_change: add https://*.googleusercontent.com https://*.googleapis.com to img-src, https://* to connect-src -->
-            <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://*.googleusercontent.com https://storage.googleapis.com https://img.clerk.com data: https://*.googleapis.com; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://* https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
+            <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://*.googleusercontent.com https://storage.googleapis.com https://img.clerk.com data: https://*.googleapis.com; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://* https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
             <link rel="stylesheet" type="text/css" href="${stylesUri}">
 			<link href="${codiconsUri}" rel="stylesheet" />
 			<script nonce="${nonce}">
@@ -1286,6 +1293,46 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 	}
 	// kilocode_change end
 
+	/**
+	 * Fetches marketplace dataon demand to avoid blocking main state updates
+	 */
+	async fetchMarketplaceData() {
+		try {
+			const [marketplaceItems, marketplaceInstalledMetadata] = await Promise.all([
+				this.marketplaceManager.getCurrentItems().catch((error) => {
+					console.error("Failed to fetch marketplace items:", error)
+					return [] as MarketplaceItem[]
+				}),
+				this.marketplaceManager.getInstallationMetadata().catch((error) => {
+					console.error("Failed to fetch installation metadata:", error)
+					return { project: {}, global: {} } as MarketplaceInstalledMetadata
+				}),
+			])
+
+			// Send marketplace data separately
+			this.postMessageToWebview({
+				type: "marketplaceData",
+				marketplaceItems: marketplaceItems || [],
+				marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} },
+			})
+		} catch (error) {
+			console.error("Failed to fetch marketplace data:", error)
+			// Send empty data on error to prevent UI from hanging
+			this.postMessageToWebview({
+				type: "marketplaceData",
+				marketplaceItems: [],
+				marketplaceInstalledMetadata: { project: {}, global: {} },
+			})
+
+			// Show user-friendly error notification for network issues
+			if (error instanceof Error && error.message.includes("timeout")) {
+				vscode.window.showWarningMessage(
+					"Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
+				)
+			}
+		}
+	}
+
 	/**
 	 * Checks if there is a file-based system prompt override for the given mode
 	 */
@@ -1368,27 +1415,19 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 			customCondensingPrompt,
 			codebaseIndexConfig,
 			codebaseIndexModels,
+			profileThresholds,
 		} = await this.getState()
 
 		const machineId = vscode.env.machineId
 		const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
 		const cwd = this.cwd
 
-		// Fetch marketplace data
-		let marketplaceItems: any[] = []
-		let marketplaceInstalledMetadata: any = { project: {}, global: {} }
-
-		marketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []
-		marketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()
-
 		// Check if there's a system prompt override for the current mode
 		const currentMode = mode ?? defaultModeSlug
 		const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
 
 		return {
 			version: this.context.extension?.packageJSON?.version ?? "",
-			marketplaceItems,
-			marketplaceInstalledMetadata,
 			apiConfiguration,
 			customInstructions,
 			alwaysAllowReadOnly: alwaysAllowReadOnly ?? true,
@@ -1484,6 +1523,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 				codebaseIndexEmbedderModelId: "",
 			},
 			mdmCompliant: this.checkMdmCompliance(),
+			profileThresholds: profileThresholds ?? {},
 		}
 	}
 
@@ -1635,6 +1675,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 				codebaseIndexEmbedderBaseUrl: "",
 				codebaseIndexEmbedderModelId: "",
 			},
+			profileThresholds: stateValues.profileThresholds ?? {},
 		}
 	}
 

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

@@ -536,6 +536,7 @@ describe("ClineProvider", () => {
 			autoCondenseContextPercent: 100,
 			cloudIsAuthenticated: false,
 			sharingEnabled: false,
+			profileThresholds: {},
 		}
 
 		const message: ExtensionMessage = {
@@ -583,6 +584,117 @@ describe("ClineProvider", () => {
 		expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
 	})
 
+	describe("clearTask message handler", () => {
+		beforeEach(async () => {
+			await provider.resolveWebviewView(mockWebviewView)
+		})
+
+		test("calls clearTask when there is no parent task", async () => {
+			// Setup a single task without parent
+			const mockCline = new Task(defaultTaskOptions)
+			// No need to set parentTask - it's undefined by default
+
+			// Mock the provider methods
+			const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
+			const finishSubTaskSpy = vi.spyOn(provider, "finishSubTask").mockResolvedValue(undefined)
+			const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
+
+			// Add task to stack
+			await provider.addClineToStack(mockCline)
+
+			// Get the message handler
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
+
+			// Trigger clearTask message
+			await messageHandler({ type: "clearTask" })
+
+			// Verify clearTask was called (not finishSubTask)
+			expect(clearTaskSpy).toHaveBeenCalled()
+			expect(finishSubTaskSpy).not.toHaveBeenCalled()
+			expect(postStateToWebviewSpy).toHaveBeenCalled()
+		})
+
+		test("calls finishSubTask when there is a parent task", async () => {
+			// Setup parent and child tasks
+			const parentTask = new Task(defaultTaskOptions)
+			const childTask = new Task(defaultTaskOptions)
+
+			// Set up parent-child relationship by setting the parentTask property
+			// The mock allows us to set properties directly
+			;(childTask as any).parentTask = parentTask
+			;(childTask as any).rootTask = parentTask
+
+			// Mock the provider methods
+			const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
+			const finishSubTaskSpy = vi.spyOn(provider, "finishSubTask").mockResolvedValue(undefined)
+			const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
+
+			// Add both tasks to stack (parent first, then child)
+			await provider.addClineToStack(parentTask)
+			await provider.addClineToStack(childTask)
+
+			// Get the message handler
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
+
+			// Trigger clearTask message
+			await messageHandler({ type: "clearTask" })
+
+			// Verify finishSubTask was called (not clearTask)
+			expect(finishSubTaskSpy).toHaveBeenCalledWith(expect.stringContaining("canceled"))
+			expect(clearTaskSpy).not.toHaveBeenCalled()
+			expect(postStateToWebviewSpy).toHaveBeenCalled()
+		})
+
+		test("handles case when no current task exists", async () => {
+			// Don't add any tasks to the stack
+
+			// Mock the provider methods
+			const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
+			const finishSubTaskSpy = vi.spyOn(provider, "finishSubTask").mockResolvedValue(undefined)
+			const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
+
+			// Get the message handler
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
+
+			// Trigger clearTask message
+			await messageHandler({ type: "clearTask" })
+
+			// When there's no current task, clearTask is still called (it handles the no-task case internally)
+			expect(clearTaskSpy).toHaveBeenCalled()
+			expect(finishSubTaskSpy).not.toHaveBeenCalled()
+			// State should still be posted
+			expect(postStateToWebviewSpy).toHaveBeenCalled()
+		})
+
+		test("correctly identifies subtask scenario for issue #4602", async () => {
+			// This test specifically validates the fix for issue #4602
+			// where canceling during API retry was incorrectly treating a single task as a subtask
+
+			const mockCline = new Task(defaultTaskOptions)
+			// No parent task by default - no need to explicitly set
+
+			// Mock the provider methods
+			const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
+			const finishSubTaskSpy = vi.spyOn(provider, "finishSubTask").mockResolvedValue(undefined)
+
+			// Add only one task to stack
+			await provider.addClineToStack(mockCline)
+
+			// Verify stack size is 1
+			expect(provider.getClineStackSize()).toBe(1)
+
+			// Get the message handler
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
+
+			// Trigger clearTask message (simulating cancel during API retry)
+			await messageHandler({ type: "clearTask" })
+
+			// The fix ensures clearTask is called, not finishSubTask
+			expect(clearTaskSpy).toHaveBeenCalled()
+			expect(finishSubTaskSpy).not.toHaveBeenCalled()
+		})
+	})
+
 	test("addClineToStack adds multiple Cline instances to the stack", async () => {
 		// Setup Cline instance with auto-mock from the top of the file
 		const mockCline1 = new Task(defaultTaskOptions) // Create a new mocked instance
@@ -2121,6 +2233,8 @@ describe("ClineProvider - Router Models", () => {
 				unbound: mockModels,
 				litellm: mockModels,
 				"kilocode-openrouter": mockModels,
+				ollama: {},
+				lmstudio: {},
 			},
 		})
 	})
@@ -2164,6 +2278,8 @@ describe("ClineProvider - Router Models", () => {
 				requesty: {},
 				glama: mockModels,
 				unbound: {},
+				ollama: {},
+				lmstudio: {},
 				litellm: {},
 				"kilocode-openrouter": {},
 			},
@@ -2191,6 +2307,13 @@ describe("ClineProvider - Router Models", () => {
 			values: { provider: "kilocode-openrouter" },
 		})
 
+		expect(mockPostMessage).toHaveBeenCalledWith({
+			type: "singleRouterModelFetchResponse",
+			success: false,
+			error: "Unbound API error",
+			values: { provider: "unbound" },
+		})
+
 		expect(mockPostMessage).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
@@ -2275,6 +2398,8 @@ describe("ClineProvider - Router Models", () => {
 				unbound: mockModels,
 				litellm: {},
 				"kilocode-openrouter": mockModels,
+				ollama: {},
+				lmstudio: {},
 			},
 		})
 	})

+ 27 - 7
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -74,6 +74,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				unbound: mockModels,
 				litellm: mockModels,
 				"kilocode-openrouter": mockModels,
+				ollama: {},
+				lmstudio: {},
 			},
 		})
 	})
@@ -160,6 +162,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				unbound: mockModels,
 				litellm: {},
 				"kilocode-openrouter": mockModels,
+				ollama: {},
+				lmstudio: {},
 			},
 		})
 	})
@@ -197,6 +201,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				unbound: {},
 				litellm: {},
 				"kilocode-openrouter": mockModels,
+				ollama: {},
+				lmstudio: {},
 			},
 		})
 
@@ -226,12 +232,12 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 	it("handles Error objects and string errors correctly", async () => {
 		// Mock providers to fail with different error types
 		mockGetModels
-			.mockRejectedValueOnce(new Error("Structured error message")) // openrouter - Error object
-			.mockRejectedValueOnce("String error message") // requesty - String error
-			.mockRejectedValueOnce({ message: "Object with message" }) // glama - Object error
-			.mockResolvedValueOnce({}) // unbound - Success
+			.mockRejectedValueOnce(new Error("Structured error message")) // openrouter
+			.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
+			.mockRejectedValueOnce(new Error("Glama API error")) // glama
+			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockResolvedValueOnce({}) // kilocode-openrouter - Success
-			.mockResolvedValueOnce({}) // litellm - Success
+			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
 			type: "requestRouterModels",
@@ -248,16 +254,30 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
-			error: "String error message",
+			error: "Requesty API error",
 			values: { provider: "requesty" },
 		})
 
 		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
 			type: "singleRouterModelFetchResponse",
 			success: false,
-			error: "[object Object]",
+			error: "Glama API error",
 			values: { provider: "glama" },
 		})
+
+		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+			type: "singleRouterModelFetchResponse",
+			success: false,
+			error: "Unbound API error",
+			values: { provider: "unbound" },
+		})
+
+		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+			type: "singleRouterModelFetchResponse",
+			success: false,
+			error: "LiteLLM connection failed",
+			values: { provider: "litellm" },
+		})
 	})
 
 	it("prefers config values over message values for LiteLLM", async () => {

+ 130 - 21
src/core/webview/webviewMessageHandler.ts

@@ -31,9 +31,7 @@ import { singleCompletionHandler } from "../../utils/single-completion-handler"
 import { searchCommits } from "../../utils/git"
 import { exportSettings, importSettings } from "../config/importExport"
 import { getOpenAiModels } from "../../api/providers/openai"
-import { getOllamaModels } from "../../api/providers/ollama"
 import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
-import { getLmStudioModels } from "../../api/providers/lm-studio"
 import { openMention } from "../mentions"
 import { TelemetrySetting } from "../../shared/TelemetrySetting"
 import { getWorkspacePath } from "../../utils/path"
@@ -207,7 +205,14 @@ export const webviewMessageHandler = async (
 			break
 		case "clearTask":
 			// clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed
-			await provider.finishSubTask(t("common:tasks.canceled"))
+			// Check if the current task actually has a parent task
+			const currentTask = provider.getCurrentCline()
+			if (currentTask && currentTask.parentTask) {
+				await provider.finishSubTask(t("common:tasks.canceled"))
+			} else {
+				// Regular task - just clear it
+				await provider.clearTask()
+			}
 			await provider.postStateToWebview()
 			break
 		case "didShowAnnouncement":
@@ -232,16 +237,31 @@ export const webviewMessageHandler = async (
 			}
 
 			try {
-				const success = await CloudService.instance.shareTask(shareTaskId)
-				if (success) {
-					// Show success message
-					vscode.window.showInformationMessage(t("common:info.share_link_copied"))
+				const visibility = message.visibility || "organization"
+				const result = await CloudService.instance.shareTask(shareTaskId, visibility)
+
+				if (result.success && result.shareUrl) {
+					// Show success notification
+					const messageKey =
+						visibility === "public"
+							? "common:info.public_share_link_copied"
+							: "common:info.organization_share_link_copied"
+					vscode.window.showInformationMessage(t(messageKey))
 				} else {
-					// Show generic failure message
-					vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
+					// Handle error
+					const errorMessage = result.error || "Failed to create share link"
+					if (errorMessage.includes("Authentication")) {
+						vscode.window.showErrorMessage(t("common:errors.share_auth_required"))
+					} else if (errorMessage.includes("sharing is not enabled")) {
+						vscode.window.showErrorMessage(t("common:errors.share_not_enabled"))
+					} else if (errorMessage.includes("not found")) {
+						vscode.window.showErrorMessage(t("common:errors.share_task_not_found"))
+					} else {
+						vscode.window.showErrorMessage(errorMessage)
+					}
 				}
 			} catch (error) {
-				// Show generic failure message
+				provider.log(`[shareCurrentTask] Unexpected error: ${error}`)
 				vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
 			}
 			break
@@ -342,6 +362,8 @@ export const webviewMessageHandler = async (
 				unbound: {},
 				litellm: {},
 				"kilocode-openrouter": {}, // kilocode_change
+				ollama: {},
+				lmstudio: {},
 			}
 
 			const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@@ -375,6 +397,9 @@ export const webviewMessageHandler = async (
 			]
 			// kilocode_change end
 
+			// 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) {
@@ -391,13 +416,31 @@ export const webviewMessageHandler = async (
 				}),
 			)
 
-			const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = { ...routerModels }
+			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
 
 				if (result.status === "fulfilled") {
 					fetchedRouterModels[routerName] = result.value.models
+
+					// Ollama and LM Studio settings pages still need these events
+					if (routerName === "ollama" && Object.keys(result.value.models).length > 0) {
+						provider.postMessageToWebview({
+							type: "ollamaModels",
+							ollamaModels: Object.keys(result.value.models),
+						})
+					} else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) {
+						provider.postMessageToWebview({
+							type: "lmStudioModels",
+							lmStudioModels: Object.keys(result.value.models),
+						})
+					}
 				} else {
 					// Handle rejection: Post a specific error message for this provider
 					const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
@@ -418,7 +461,56 @@ export const webviewMessageHandler = async (
 				type: "routerModels",
 				routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
 			})
+
+			break
+		case "requestOllamaModels": {
+			// Specific handler for Ollama models only
+			const { apiConfiguration: ollamaApiConfig } = await provider.getState()
+			try {
+				// Flush cache first to ensure fresh models
+				await flushModels("ollama")
+
+				const ollamaModels = await getModels({
+					provider: "ollama",
+					baseUrl: ollamaApiConfig.ollamaBaseUrl,
+				})
+
+				if (Object.keys(ollamaModels).length > 0) {
+					provider.postMessageToWebview({
+						type: "ollamaModels",
+						ollamaModels: Object.keys(ollamaModels),
+					})
+				}
+			} catch (error) {
+				// Silently fail - user hasn't configured Ollama yet
+				console.debug("Ollama models fetch failed:", error)
+			}
 			break
+		}
+		case "requestLmStudioModels": {
+			// Specific handler for LM Studio models only
+			const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
+			try {
+				// Flush cache first to ensure fresh models
+				await flushModels("lmstudio")
+
+				const lmStudioModels = await getModels({
+					provider: "lmstudio",
+					baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
+				})
+
+				if (Object.keys(lmStudioModels).length > 0) {
+					provider.postMessageToWebview({
+						type: "lmStudioModels",
+						lmStudioModels: Object.keys(lmStudioModels),
+					})
+				}
+			} catch (error) {
+				// Silently fail - user hasn't configured LM Studio yet
+				console.debug("LM Studio models fetch failed:", error)
+			}
+			break
+		}
 		case "requestOpenAiModels":
 			if (message?.values?.baseUrl && message?.values?.apiKey) {
 				const openAiModels = await getOpenAiModels(
@@ -430,16 +522,6 @@ export const webviewMessageHandler = async (
 				provider.postMessageToWebview({ type: "openAiModels", openAiModels })
 			}
 
-			break
-		case "requestOllamaModels":
-			const ollamaModels = await getOllamaModels(message.text)
-			// TODO: Cache like we do for OpenRouter, etc?
-			provider.postMessageToWebview({ type: "ollamaModels", ollamaModels })
-			break
-		case "requestLmStudioModels":
-			const lmStudioModels = await getLmStudioModels(message.text)
-			// TODO: Cache like we do for OpenRouter, etc?
-			provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
 			break
 		case "requestVsCodeLmModels":
 			const vsCodeLmModels = await getVsCodeLmModels()
@@ -604,6 +686,23 @@ export const webviewMessageHandler = async (
 			}
 			break
 		}
+		case "toggleToolEnabledForPrompt": {
+			try {
+				await provider
+					.getMcpHub()
+					?.toggleToolEnabledForPrompt(
+						message.serverName!,
+						message.source as "global" | "project",
+						message.toolName!,
+						Boolean(message.isEnabled),
+					)
+			} catch (error) {
+				provider.log(
+					`Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+				)
+			}
+			break
+		}
 		case "toggleMcpServer": {
 			try {
 				await provider
@@ -1084,6 +1183,10 @@ export const webviewMessageHandler = async (
 			await updateGlobalState("customCondensingPrompt", message.text)
 			await provider.postStateToWebview()
 			break
+		case "profileThresholds":
+			await updateGlobalState("profileThresholds", message.values)
+			await provider.postStateToWebview()
+			break
 		case "autoApprovalEnabled":
 			await updateGlobalState("autoApprovalEnabled", message.bool ?? false)
 			await provider.postStateToWebview()
@@ -1731,6 +1834,12 @@ export const webviewMessageHandler = async (
 			break
 		}
 
+		case "fetchMarketplaceData": {
+			// Fetch marketplace data on demand
+			await provider.fetchMarketplaceData()
+			break
+		}
+
 		case "installMarketplaceItem": {
 			if (marketplaceManager && message.mpItem && message.mpInstallOptions) {
 				try {

+ 20 - 3
src/i18n/locales/ca/common.json

@@ -71,7 +71,17 @@
 		"condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid",
 		"condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent",
 		"share_task_failed": "Ha fallat compartir la tasca. Si us plau, torna-ho a provar.",
-		"share_no_active_task": "No hi ha cap tasca activa per compartir"
+		"share_no_active_task": "No hi ha cap tasca activa per compartir",
+		"share_auth_required": "Es requereix autenticació. Si us plau, inicia sessió per compartir tasques.",
+		"share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.",
+		"share_task_not_found": "Tasca no trobada o accés denegat.",
+		"claudeCode": {
+			"processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.",
+			"errorOutput": "Sortida d'error: {{output}}",
+			"processExitedWithError": "El procés Claude Code ha sortit amb codi {{exitCode}}. Sortida d'error: {{output}}",
+			"stoppedWithReason": "Claude Code s'ha aturat per la raó: {{reason}}",
+			"apiKeyModelPlanMismatch": "Les claus API i els plans de subscripció permeten models diferents. Assegura't que el model seleccionat estigui inclòs al teu pla."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "No s'ha seleccionat contingut de terminal",
@@ -86,7 +96,9 @@
 		"settings_imported": "Configuració importada correctament.",
 		"share_link_copied": "Enllaç de compartició copiat al portapapers",
 		"image_copied_to_clipboard": "URI de dades de la imatge copiada al portapapers",
-		"image_saved": "Imatge desada a {{path}}"
+		"image_saved": "Imatge desada a {{path}}",
+		"organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!",
+		"public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!"
 	},
 	"answers": {
 		"yes": "Sí",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Clau API de Groq",
-			"getGroqApiKey": "Obté la clau API de Groq"
+			"getGroqApiKey": "Obté la clau API de Groq",
+			"claudeCode": {
+				"pathLabel": "Ruta de Claude Code",
+				"description": "Ruta opcional a la teva CLI de Claude Code. Per defecte 'claude' si no s'estableix.",
+				"placeholder": "Per defecte: claude"
+			}
 		}
 	},
 	"mdm": {

+ 20 - 3
src/i18n/locales/de/common.json

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig",
 		"condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen",
 		"share_task_failed": "Teilen der Aufgabe fehlgeschlagen. Bitte versuche es erneut.",
-		"share_no_active_task": "Keine aktive Aufgabe zum Teilen"
+		"share_no_active_task": "Keine aktive Aufgabe zum Teilen",
+		"share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.",
+		"share_not_enabled": "Aufgabenfreigabe ist für diese Organisation nicht aktiviert.",
+		"share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert.",
+		"claudeCode": {
+			"processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.",
+			"errorOutput": "Fehlerausgabe: {{output}}",
+			"processExitedWithError": "Claude Code Prozess wurde mit Code {{exitCode}} beendet. Fehlerausgabe: {{output}}",
+			"stoppedWithReason": "Claude Code wurde mit Grund gestoppt: {{reason}}",
+			"apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Kein Terminal-Inhalt ausgewählt",
@@ -82,7 +92,9 @@
 		"settings_imported": "Einstellungen erfolgreich importiert.",
 		"share_link_copied": "Share-Link in die Zwischenablage kopiert",
 		"image_copied_to_clipboard": "Bild-Daten-URI in die Zwischenablage kopiert",
-		"image_saved": "Bild gespeichert unter {{path}}"
+		"image_saved": "Bild gespeichert unter {{path}}",
+		"organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!",
+		"public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!"
 	},
 	"answers": {
 		"yes": "Ja",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Groq API-Schlüssel",
-			"getGroqApiKey": "Groq API-Schlüssel erhalten"
+			"getGroqApiKey": "Groq API-Schlüssel erhalten",
+			"claudeCode": {
+				"pathLabel": "Claude Code Pfad",
+				"description": "Optionaler Pfad zu deiner Claude Code CLI. Standardmäßig 'claude', falls nicht festgelegt.",
+				"placeholder": "Standard: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "API handler for condensing context is invalid",
 		"condense_context_grew": "Context size increased during condensing; skipping this attempt",
 		"share_task_failed": "Failed to share task. Please try again.",
-		"share_no_active_task": "No active task to share"
+		"share_no_active_task": "No active task to share",
+		"share_auth_required": "Authentication required. Please sign in to share tasks.",
+		"share_not_enabled": "Task sharing is not enabled for this organization.",
+		"share_task_not_found": "Task not found or access denied.",
+		"claudeCode": {
+			"processExited": "Claude Code process exited with code {{exitCode}}.",
+			"errorOutput": "Error output: {{output}}",
+			"processExitedWithError": "Claude Code process exited with code {{exitCode}}. Error output: {{output}}",
+			"stoppedWithReason": "Claude Code stopped with reason: {{reason}}",
+			"apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "No terminal content selected",
@@ -81,6 +91,8 @@
 		"default_storage_path": "Reverted to using default storage path",
 		"settings_imported": "Settings imported successfully.",
 		"share_link_copied": "Share link copied to clipboard",
+		"organization_share_link_copied": "Organization share link copied to clipboard!",
+		"public_share_link_copied": "Public share link copied to clipboard!",
 		"image_copied_to_clipboard": "Image data URI copied to clipboard",
 		"image_saved": "Image saved to {{path}}"
 	},

+ 20 - 3
src/i18n/locales/es/common.json

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "El manejador de API para condensar el contexto no es válido",
 		"condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento",
 		"share_task_failed": "Error al compartir la tarea. Por favor, inténtalo de nuevo.",
-		"share_no_active_task": "No hay tarea activa para compartir"
+		"share_no_active_task": "No hay tarea activa para compartir",
+		"share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.",
+		"share_not_enabled": "La compartición de tareas no está habilitada para esta organización.",
+		"share_task_not_found": "Tarea no encontrada o acceso denegado.",
+		"claudeCode": {
+			"processExited": "El proceso de Claude Code terminó con código {{exitCode}}.",
+			"errorOutput": "Salida de error: {{output}}",
+			"processExitedWithError": "El proceso de Claude Code terminó con código {{exitCode}}. Salida de error: {{output}}",
+			"stoppedWithReason": "Claude Code se detuvo por la razón: {{reason}}",
+			"apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "No hay contenido de terminal seleccionado",
@@ -82,7 +92,9 @@
 		"settings_imported": "Configuración importada correctamente.",
 		"share_link_copied": "Enlace de compartir copiado al portapapeles",
 		"image_copied_to_clipboard": "URI de datos de imagen copiada al portapapeles",
-		"image_saved": "Imagen guardada en {{path}}"
+		"image_saved": "Imagen guardada en {{path}}",
+		"organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!",
+		"public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!"
 	},
 	"answers": {
 		"yes": "Sí",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Clave API de Groq",
-			"getGroqApiKey": "Obtener clave API de Groq"
+			"getGroqApiKey": "Obtener clave API de Groq",
+			"claudeCode": {
+				"pathLabel": "Ruta de Claude Code",
+				"description": "Ruta opcional a tu CLI de Claude Code. Por defecto 'claude' si no se establece.",
+				"placeholder": "Por defecto: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide",
 		"condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée",
 		"share_task_failed": "Échec du partage de la tâche. Veuillez réessayer.",
-		"share_no_active_task": "Aucune tâche active à partager"
+		"share_no_active_task": "Aucune tâche active à partager",
+		"share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.",
+		"share_not_enabled": "Le partage de tâches n'est pas activé pour cette organisation.",
+		"share_task_not_found": "Tâche non trouvée ou accès refusé.",
+		"claudeCode": {
+			"processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.",
+			"errorOutput": "Sortie d'erreur : {{output}}",
+			"processExitedWithError": "Le processus Claude Code s'est terminé avec le code {{exitCode}}. Sortie d'erreur : {{output}}",
+			"stoppedWithReason": "Claude Code s'est arrêté pour la raison : {{reason}}",
+			"apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Aucun contenu de terminal sélectionné",
@@ -82,7 +92,9 @@
 		"settings_imported": "Paramètres importés avec succès.",
 		"share_link_copied": "Lien de partage copié dans le presse-papiers",
 		"image_copied_to_clipboard": "URI de données d'image copiée dans le presse-papiers",
-		"image_saved": "Image enregistrée dans {{path}}"
+		"image_saved": "Image enregistrée dans {{path}}",
+		"organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !",
+		"public_share_link_copied": "Lien de partage public copié dans le presse-papiers !"
 	},
 	"answers": {
 		"yes": "Oui",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Clé API Groq",
-			"getGroqApiKey": "Obtenir la clé API Groq"
+			"getGroqApiKey": "Obtenir la clé API Groq",
+			"claudeCode": {
+				"pathLabel": "Chemin de Claude Code",
+				"description": "Chemin optionnel vers votre CLI Claude Code. Par défaut 'claude' si non défini.",
+				"placeholder": "Par défaut : claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "संदर्भ को संक्षिप्त करने के लिए API हैंडलर अमान्य है",
 		"condense_context_grew": "संक्षिप्तीकरण के दौरान संदर्भ का आकार बढ़ गया; इस प्रयास को छोड़ा जा रहा है",
 		"share_task_failed": "कार्य साझा करने में विफल। कृपया पुनः प्रयास करें।",
-		"share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं"
+		"share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं",
+		"share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।",
+		"share_not_enabled": "इस संगठन के लिए कार्य साझाकरण सक्षम नहीं है।",
+		"share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।",
+		"claudeCode": {
+			"processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।",
+			"errorOutput": "त्रुटि आउटपुट: {{output}}",
+			"processExitedWithError": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई। त्रुटि आउटपुट: {{output}}",
+			"stoppedWithReason": "Claude Code इस कारण से रुका: {{reason}}",
+			"apiKeyModelPlanMismatch": "API कुंजी और सब्सक्रिप्शन प्लान अलग-अलग मॉडल की अनुमति देते हैं। सुनिश्चित करें कि चयनित मॉडल आपकी योजना में शामिल है।"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं",
@@ -82,7 +92,9 @@
 		"settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।",
 		"share_link_copied": "साझा लिंक क्लिपबोर्ड पर कॉपी किया गया",
 		"image_copied_to_clipboard": "छवि डेटा URI क्लिपबोर्ड में कॉपी की गई",
-		"image_saved": "छवि {{path}} में सहेजी गई"
+		"image_saved": "छवि {{path}} में सहेजी गई",
+		"organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
+		"public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!"
 	},
 	"answers": {
 		"yes": "हां",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "ग्रोक एपीआई कुंजी",
-			"getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें"
+			"getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें",
+			"claudeCode": {
+				"pathLabel": "क्लाउड कोड पाथ",
+				"description": "आपके क्लाउड कोड CLI का वैकल्पिक पाथ। सेट न होने पर डिफ़ॉल्ट रूप से 'claude'。",
+				"placeholder": "डिफ़ॉल्ट: claude"
+			}
 		}
 	},
 	"mdm": {

+ 25 - 2
src/i18n/locales/id/common.json

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "Handler API untuk mengompres konteks tidak valid",
 		"condense_context_grew": "Ukuran konteks bertambah saat mengompres; melewati percobaan ini",
 		"share_task_failed": "Gagal membagikan tugas. Silakan coba lagi.",
-		"share_no_active_task": "Tidak ada tugas aktif untuk dibagikan"
+		"share_no_active_task": "Tidak ada tugas aktif untuk dibagikan",
+		"share_auth_required": "Autentikasi diperlukan. Silakan masuk untuk berbagi tugas.",
+		"share_not_enabled": "Berbagi tugas tidak diaktifkan untuk organisasi ini.",
+		"share_task_not_found": "Tugas tidak ditemukan atau akses ditolak.",
+		"claudeCode": {
+			"processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.",
+			"errorOutput": "Output error: {{output}}",
+			"processExitedWithError": "Proses Claude Code keluar dengan kode {{exitCode}}. Output error: {{output}}",
+			"stoppedWithReason": "Claude Code berhenti karena alasan: {{reason}}",
+			"apiKeyModelPlanMismatch": "Kunci API dan paket berlangganan memungkinkan model yang berbeda. Pastikan model yang dipilih termasuk dalam paket Anda."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Tidak ada konten terminal yang dipilih",
@@ -82,7 +92,9 @@
 		"settings_imported": "Pengaturan berhasil diimpor.",
 		"share_link_copied": "Link bagikan disalin ke clipboard",
 		"image_copied_to_clipboard": "Data URI gambar disalin ke clipboard",
-		"image_saved": "Gambar disimpan ke {{path}}"
+		"image_saved": "Gambar disimpan ke {{path}}",
+		"organization_share_link_copied": "Tautan berbagi organisasi disalin ke clipboard!",
+		"public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!"
 	},
 	"answers": {
 		"yes": "Ya",
@@ -105,6 +117,17 @@
 		"task_prompt": "Apa yang harus Kilo Code lakukan?",
 		"task_placeholder": "Ketik tugas kamu di sini"
 	},
+	"settings": {
+		"providers": {
+			"groqApiKey": "Kunci API Groq",
+			"getGroqApiKey": "Dapatkan Kunci API Groq",
+			"claudeCode": {
+				"pathLabel": "Jalur Claude Code",
+				"description": "Jalur opsional ke CLI Claude Code Anda. Defaultnya 'claude' jika tidak diatur.",
+				"placeholder": "Default: claude"
+			}
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.",

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "Il gestore API per condensare il contesto non è valido",
 		"condense_context_grew": "La dimensione del contesto è aumentata durante la condensazione; questo tentativo viene saltato",
 		"share_task_failed": "Condivisione dell'attività fallita. Riprova.",
-		"share_no_active_task": "Nessuna attività attiva da condividere"
+		"share_no_active_task": "Nessuna attività attiva da condividere",
+		"share_auth_required": "Autenticazione richiesta. Accedi per condividere le attività.",
+		"share_not_enabled": "La condivisione delle attività non è abilitata per questa organizzazione.",
+		"share_task_not_found": "Attività non trovata o accesso negato.",
+		"claudeCode": {
+			"processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.",
+			"errorOutput": "Output di errore: {{output}}",
+			"processExitedWithError": "Il processo Claude Code è terminato con codice {{exitCode}}. Output di errore: {{output}}",
+			"stoppedWithReason": "Claude Code si è fermato per il motivo: {{reason}}",
+			"apiKeyModelPlanMismatch": "Le chiavi API e i piani di abbonamento consentono modelli diversi. Assicurati che il modello selezionato sia incluso nel tuo piano."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nessun contenuto del terminale selezionato",
@@ -82,7 +92,9 @@
 		"settings_imported": "Impostazioni importate con successo.",
 		"share_link_copied": "Link di condivisione copiato negli appunti",
 		"image_copied_to_clipboard": "URI dati dell'immagine copiato negli appunti",
-		"image_saved": "Immagine salvata in {{path}}"
+		"image_saved": "Immagine salvata in {{path}}",
+		"organization_share_link_copied": "Link di condivisione organizzazione copiato negli appunti!",
+		"public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!"
 	},
 	"answers": {
 		"yes": "Sì",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Chiave API Groq",
-			"getGroqApiKey": "Ottieni chiave API Groq"
+			"getGroqApiKey": "Ottieni chiave API Groq",
+			"claudeCode": {
+				"pathLabel": "Percorso Claude Code",
+				"description": "Percorso opzionale alla tua CLI Claude Code. Predefinito 'claude' se non impostato.",
+				"placeholder": "Predefinito: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "コンテキストを圧縮するためのAPIハンドラーが無効です",
 		"condense_context_grew": "圧縮中にコンテキストサイズが増加しました;この試行をスキップします",
 		"share_task_failed": "タスクの共有に失敗しました",
-		"share_no_active_task": "共有するアクティブなタスクがありません"
+		"share_no_active_task": "共有するアクティブなタスクがありません",
+		"share_auth_required": "認証が必要です。タスクを共有するにはサインインしてください。",
+		"share_not_enabled": "この組織ではタスク共有が有効になっていません。",
+		"share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。",
+		"claudeCode": {
+			"processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。",
+			"errorOutput": "エラー出力:{{output}}",
+			"processExitedWithError": "Claude Code プロセスがコード {{exitCode}} で終了しました。エラー出力:{{output}}",
+			"stoppedWithReason": "Claude Code が理由により停止しました:{{reason}}",
+			"apiKeyModelPlanMismatch": "API キーとサブスクリプションプランでは異なるモデルが利用可能です。選択したモデルがプランに含まれていることを確認してください。"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "選択されたターミナルコンテンツがありません",
@@ -82,7 +92,9 @@
 		"settings_imported": "設定が正常にインポートされました。",
 		"share_link_copied": "共有リンクがクリップボードにコピーされました",
 		"image_copied_to_clipboard": "画像データURIがクリップボードにコピーされました",
-		"image_saved": "画像を{{path}}に保存しました"
+		"image_saved": "画像を{{path}}に保存しました",
+		"organization_share_link_copied": "組織共有リンクがクリップボードにコピーされました!",
+		"public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!"
 	},
 	"answers": {
 		"yes": "はい",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Groq APIキー",
-			"getGroqApiKey": "Groq APIキーを取得"
+			"getGroqApiKey": "Groq APIキーを取得",
+			"claudeCode": {
+				"pathLabel": "Claude Code パス",
+				"description": "Claude Code CLI へのオプションのパス。設定されていない場合は、デフォルトで「claude」になります。",
+				"placeholder": "デフォルト: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "컨텍스트 압축을 위한 API 핸들러가 유효하지 않습니다",
 		"condense_context_grew": "압축 중 컨텍스트 크기가 증가했습니다; 이 시도를 건너뜁니다",
 		"share_task_failed": "작업 공유에 실패했습니다",
-		"share_no_active_task": "공유할 활성 작업이 없습니다"
+		"share_no_active_task": "공유할 활성 작업이 없습니다",
+		"share_auth_required": "인증이 필요합니다. 작업을 공유하려면 로그인하세요.",
+		"share_not_enabled": "이 조직에서는 작업 공유가 활성화되지 않았습니다.",
+		"share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.",
+		"claudeCode": {
+			"processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.",
+			"errorOutput": "오류 출력: {{output}}",
+			"processExitedWithError": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다. 오류 출력: {{output}}",
+			"stoppedWithReason": "Claude Code가 다음 이유로 중지되었습니다: {{reason}}",
+			"apiKeyModelPlanMismatch": "API 키와 구독 플랜에서 다른 모델을 허용합니다. 선택한 모델이 플랜에 포함되어 있는지 확인하세요."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "선택된 터미널 내용이 없습니다",
@@ -82,7 +92,9 @@
 		"settings_imported": "설정이 성공적으로 가져와졌습니다.",
 		"share_link_copied": "공유 링크가 클립보드에 복사되었습니다",
 		"image_copied_to_clipboard": "이미지 데이터 URI가 클립보드에 복사되었습니다",
-		"image_saved": "이미지가 {{path}}에 저장되었습니다"
+		"image_saved": "이미지가 {{path}}에 저장되었습니다",
+		"organization_share_link_copied": "조직 공유 링크가 클립보드에 복사되었습니다!",
+		"public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!"
 	},
 	"answers": {
 		"yes": "예",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Groq API 키",
-			"getGroqApiKey": "Groq API 키 받기"
+			"getGroqApiKey": "Groq API 키 받기",
+			"claudeCode": {
+				"pathLabel": "Claude Code 경로",
+				"description": "Claude Code CLI의 선택적 경로입니다. 설정되지 않은 경우 기본값은 'claude'입니다.",
+				"placeholder": "기본값: claude"
+			}
 		}
 	},
 	"mdm": {

+ 25 - 2
src/i18n/locales/nl/common.json

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "API-handler voor het comprimeren van context is ongeldig",
 		"condense_context_grew": "Contextgrootte nam toe tijdens comprimeren; deze poging wordt overgeslagen",
 		"share_task_failed": "Delen van taak mislukt",
-		"share_no_active_task": "Geen actieve taak om te delen"
+		"share_no_active_task": "Geen actieve taak om te delen",
+		"share_auth_required": "Authenticatie vereist. Log in om taken te delen.",
+		"share_not_enabled": "Taken delen is niet ingeschakeld voor deze organisatie.",
+		"share_task_not_found": "Taak niet gevonden of toegang geweigerd.",
+		"claudeCode": {
+			"processExited": "Claude Code proces beëindigd met code {{exitCode}}.",
+			"errorOutput": "Foutuitvoer: {{output}}",
+			"processExitedWithError": "Claude Code proces beëindigd met code {{exitCode}}. Foutuitvoer: {{output}}",
+			"stoppedWithReason": "Claude Code gestopt om reden: {{reason}}",
+			"apiKeyModelPlanMismatch": "API-sleutels en abonnementsplannen staan verschillende modellen toe. Zorg ervoor dat het geselecteerde model is opgenomen in je plan."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Geen terminalinhoud geselecteerd",
@@ -82,7 +92,9 @@
 		"settings_imported": "Instellingen succesvol geïmporteerd.",
 		"share_link_copied": "Deellink gekopieerd naar klembord",
 		"image_copied_to_clipboard": "Afbeelding data-URI gekopieerd naar klembord",
-		"image_saved": "Afbeelding opgeslagen naar {{path}}"
+		"image_saved": "Afbeelding opgeslagen naar {{path}}",
+		"organization_share_link_copied": "Organisatie deel-link gekopieerd naar klembord!",
+		"public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!"
 	},
 	"answers": {
 		"yes": "Ja",
@@ -105,6 +117,17 @@
 		"task_prompt": "Wat moet Kilo Code doen?",
 		"task_placeholder": "Typ hier je taak"
 	},
+	"settings": {
+		"providers": {
+			"groqApiKey": "Groq API-sleutel",
+			"getGroqApiKey": "Groq API-sleutel ophalen",
+			"claudeCode": {
+				"pathLabel": "Claude Code Pad",
+				"description": "Optioneel pad naar je Claude Code CLI. Standaard 'claude' indien niet ingesteld.",
+				"placeholder": "Standaard: claude"
+			}
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.",

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "Nieprawidłowy handler API do kondensowania kontekstu",
 		"condense_context_grew": "Rozmiar kontekstu wzrósł podczas kondensacji; pomijanie tej próby",
 		"share_task_failed": "Nie udało się udostępnić zadania",
-		"share_no_active_task": "Brak aktywnego zadania do udostępnienia"
+		"share_no_active_task": "Brak aktywnego zadania do udostępnienia",
+		"share_auth_required": "Wymagana autoryzacja. Zaloguj się, aby udostępniać zadania.",
+		"share_not_enabled": "Udostępnianie zadań nie jest włączone dla tej organizacji.",
+		"share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony.",
+		"claudeCode": {
+			"processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.",
+			"errorOutput": "Wyjście błędu: {{output}}",
+			"processExitedWithError": "Proces Claude Code zakończył się kodem {{exitCode}}. Wyjście błędu: {{output}}",
+			"stoppedWithReason": "Claude Code zatrzymał się z powodu: {{reason}}",
+			"apiKeyModelPlanMismatch": "Klucze API i plany subskrypcji pozwalają na różne modele. Upewnij się, że wybrany model jest zawarty w twoim planie."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nie wybrano zawartości terminala",
@@ -82,7 +92,9 @@
 		"settings_imported": "Ustawienia zaimportowane pomyślnie.",
 		"share_link_copied": "Link udostępniania skopiowany do schowka",
 		"image_copied_to_clipboard": "URI danych obrazu skopiowane do schowka",
-		"image_saved": "Obraz zapisany w {{path}}"
+		"image_saved": "Obraz zapisany w {{path}}",
+		"organization_share_link_copied": "Link udostępniania organizacji skopiowany do schowka!",
+		"public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!"
 	},
 	"answers": {
 		"yes": "Tak",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Klucz API Groq",
-			"getGroqApiKey": "Uzyskaj klucz API Groq"
+			"getGroqApiKey": "Uzyskaj klucz API Groq",
+			"claudeCode": {
+				"pathLabel": "Ścieżka Claude Code",
+				"description": "Opcjonalna ścieżka do Twojego CLI Claude Code. Domyślnie 'claude', jeśli nie ustawiono.",
+				"placeholder": "Domyślnie: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -71,7 +71,17 @@
 		"condense_handler_invalid": "O manipulador de API para condensar o contexto é inválido",
 		"condense_context_grew": "O tamanho do contexto aumentou durante a condensação; pulando esta tentativa",
 		"share_task_failed": "Falha ao compartilhar tarefa",
-		"share_no_active_task": "Nenhuma tarefa ativa para compartilhar"
+		"share_no_active_task": "Nenhuma tarefa ativa para compartilhar",
+		"share_auth_required": "Autenticação necessária. Faça login para compartilhar tarefas.",
+		"share_not_enabled": "O compartilhamento de tarefas não está habilitado para esta organização.",
+		"share_task_not_found": "Tarefa não encontrada ou acesso negado.",
+		"claudeCode": {
+			"processExited": "O processo Claude Code saiu com código {{exitCode}}.",
+			"errorOutput": "Saída de erro: {{output}}",
+			"processExitedWithError": "O processo Claude Code saiu com código {{exitCode}}. Saída de erro: {{output}}",
+			"stoppedWithReason": "Claude Code parou pela razão: {{reason}}",
+			"apiKeyModelPlanMismatch": "Chaves de API e planos de assinatura permitem modelos diferentes. Certifique-se de que o modelo selecionado esteja incluído no seu plano."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nenhum conteúdo do terminal selecionado",
@@ -86,7 +96,9 @@
 		"settings_imported": "Configurações importadas com sucesso.",
 		"share_link_copied": "Link de compartilhamento copiado para a área de transferência",
 		"image_copied_to_clipboard": "URI de dados da imagem copiada para a área de transferência",
-		"image_saved": "Imagem salva em {{path}}"
+		"image_saved": "Imagem salva em {{path}}",
+		"organization_share_link_copied": "Link de compartilhamento da organização copiado para a área de transferência!",
+		"public_share_link_copied": "Link de compartilhamento público copiado para a área de transferência!"
 	},
 	"answers": {
 		"yes": "Sim",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Chave de API Groq",
-			"getGroqApiKey": "Obter chave de API Groq"
+			"getGroqApiKey": "Obter chave de API Groq",
+			"claudeCode": {
+				"pathLabel": "Caminho do Claude Code",
+				"description": "Caminho opcional para sua CLI do Claude Code. Padrão 'claude' se não for definido.",
+				"placeholder": "Padrão: claude"
+			}
 		}
 	},
 	"mdm": {

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

@@ -67,7 +67,17 @@
 		"condense_handler_invalid": "Обработчик API для сжатия контекста недействителен",
 		"condense_context_grew": "Размер контекста увеличился во время сжатия; пропускаем эту попытку",
 		"share_task_failed": "Не удалось поделиться задачей",
-		"share_no_active_task": "Нет активной задачи для совместного использования"
+		"share_no_active_task": "Нет активной задачи для совместного использования",
+		"share_auth_required": "Требуется аутентификация. Войдите в систему для совместного доступа к задачам.",
+		"share_not_enabled": "Совместный доступ к задачам не включен для этой организации.",
+		"share_task_not_found": "Задача не найдена или доступ запрещен.",
+		"claudeCode": {
+			"processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.",
+			"errorOutput": "Вывод ошибки: {{output}}",
+			"processExitedWithError": "Процесс Claude Code завершился с кодом {{exitCode}}. Вывод ошибки: {{output}}",
+			"stoppedWithReason": "Claude Code остановился по причине: {{reason}}",
+			"apiKeyModelPlanMismatch": "API-ключи и планы подписки позволяют использовать разные модели. Убедитесь, что выбранная модель включена в ваш план."
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Не выбрано содержимое терминала",
@@ -82,7 +92,9 @@
 		"settings_imported": "Настройки успешно импортированы.",
 		"share_link_copied": "Ссылка для совместного использования скопирована в буфер обмена",
 		"image_copied_to_clipboard": "URI данных изображения скопирован в буфер обмена",
-		"image_saved": "Изображение сохранено в {{path}}"
+		"image_saved": "Изображение сохранено в {{path}}",
+		"organization_share_link_copied": "Ссылка для совместного доступа организации скопирована в буфер обмена!",
+		"public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!"
 	},
 	"answers": {
 		"yes": "Да",
@@ -108,7 +120,12 @@
 	"settings": {
 		"providers": {
 			"groqApiKey": "Ключ API Groq",
-			"getGroqApiKey": "Получить ключ API Groq"
+			"getGroqApiKey": "Получить ключ API Groq",
+			"claudeCode": {
+				"pathLabel": "Путь к Claude Code",
+				"description": "Необязательный путь к вашему CLI Claude Code. По умолчанию 'claude', если не установлено.",
+				"placeholder": "По умолчанию: claude"
+			}
 		}
 	},
 	"mdm": {

Неке датотеке нису приказане због велике количине промена