Skip to content

Commit 64b57a1

Browse files
committed
Merge remote-tracking branch 'upstream/main' into see-all-task-settings
2 parents c4d2995 + 08c9420 commit 64b57a1

36 files changed

+1021
-164
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Roo Code Changelog
22

3+
## [3.23.5] - 2025-07-09
4+
5+
- Fix: use decodeURIComponent in openFile (thanks @vivekfyi!)
6+
- Fix(embeddings): Translate error messages before sending to UI (thanks @daniel-lxs!)
7+
- Make account tab visible
8+
9+
## [3.23.4] - 2025-07-09
10+
11+
- Update chat area icons for better discoverability & consistency
12+
- Fix a bug that allowed `list_files` to return directory results that should be excluded by .gitignore
13+
- Add an overflow header menu to make the UI a little tidier (thanks @dlab-anton)
14+
- Fix a bug the issue where null custom modes configuration files cause a 'Cannot read properties of null' error (thanks @daniel-lxs!)
15+
- Replace native title attributes with StandardTooltip component for consistency (thanks @daniel-lxs!)
16+
17+
## [3.23.3] - 2025-07-09
18+
19+
- Remove erroneous line from announcement modal
20+
21+
## [3.23.2] - 2025-07-09
22+
23+
- Fix bug where auto-approval was intermittently failing
24+
325
## [3.23.1] - 2025-07-09
426

527
- Always show the code indexing dot under the chat text area

src/activate/registerCommands.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRe
1414
import { handleNewTask } from "./handleTask"
1515
import { CodeIndexManager } from "../services/code-index/manager"
1616
import { importSettingsWithFeedback } from "../core/config/importExport"
17+
import { MdmService } from "../services/mdm/MdmService"
1718
import { t } from "../i18n"
1819

1920
/**
@@ -226,7 +227,17 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
226227
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
227228
const contextProxy = await ContextProxy.getInstance(context)
228229
const codeIndexManager = CodeIndexManager.getInstance(context)
229-
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager)
230+
231+
// Get the existing MDM service instance to ensure consistent policy enforcement
232+
let mdmService: MdmService | undefined
233+
try {
234+
mdmService = MdmService.getInstance()
235+
} catch (error) {
236+
// MDM service not initialized, which is fine - extension can work without it
237+
mdmService = undefined
238+
}
239+
240+
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager, mdmService)
230241
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
231242

232243
// Check if there are any visible text editors, otherwise open a new group

src/core/config/CustomModesManager.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ export class CustomModesManager {
148148
cleanedContent = this.cleanInvisibleCharacters(cleanedContent)
149149

150150
try {
151-
return yaml.parse(cleanedContent)
151+
const parsed = yaml.parse(cleanedContent)
152+
// Ensure we never return null or undefined
153+
return parsed ?? {}
152154
} catch (yamlError) {
153155
// For .roomodes files, try JSON as fallback
154156
if (filePath.endsWith(ROOMODES_FILENAME)) {
@@ -180,6 +182,12 @@ export class CustomModesManager {
180182
try {
181183
const content = await fs.readFile(filePath, "utf-8")
182184
const settings = this.parseYamlSafely(content, filePath)
185+
186+
// Ensure settings has customModes property
187+
if (!settings || typeof settings !== "object" || !settings.customModes) {
188+
return []
189+
}
190+
183191
const result = customModesSettingsSchema.safeParse(settings)
184192

185193
if (!result.success) {
@@ -458,7 +466,15 @@ export class CustomModesManager {
458466
settings = { customModes: [] }
459467
}
460468

461-
settings.customModes = operation(settings.customModes || [])
469+
// Ensure settings is an object and has customModes property
470+
if (!settings || typeof settings !== "object") {
471+
settings = { customModes: [] }
472+
}
473+
if (!settings.customModes) {
474+
settings.customModes = []
475+
}
476+
477+
settings.customModes = operation(settings.customModes)
462478
await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8")
463479
}
464480

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import * as path from "path"
4+
import * as os from "os"
5+
import { openFile } from "../open-file"
6+
7+
// Mock vscode module
8+
vi.mock("vscode", () => ({
9+
Uri: {
10+
file: vi.fn((path: string) => ({ fsPath: path })),
11+
},
12+
workspace: {
13+
fs: {
14+
stat: vi.fn(),
15+
writeFile: vi.fn(),
16+
},
17+
openTextDocument: vi.fn(),
18+
},
19+
window: {
20+
showTextDocument: vi.fn(),
21+
showErrorMessage: vi.fn(),
22+
tabGroups: {
23+
all: [],
24+
},
25+
activeTextEditor: undefined,
26+
},
27+
commands: {
28+
executeCommand: vi.fn(),
29+
},
30+
FileType: {
31+
Directory: 2,
32+
File: 1,
33+
},
34+
Selection: vi.fn((startLine: number, startChar: number, endLine: number, endChar: number) => ({
35+
start: { line: startLine, character: startChar },
36+
end: { line: endLine, character: endChar },
37+
})),
38+
TabInputText: vi.fn(),
39+
}))
40+
41+
// Mock utils
42+
vi.mock("../../utils/path", () => {
43+
const nodePath = require("path")
44+
return {
45+
arePathsEqual: vi.fn((a: string, b: string) => a === b),
46+
getWorkspacePath: vi.fn(() => {
47+
// In tests, we need to return a consistent workspace path
48+
// The actual workspace is /Users/roocode/rc2 in local, but varies in CI
49+
const cwd = process.cwd()
50+
// If we're in the src directory, go up one level to get workspace root
51+
if (cwd.endsWith("/src")) {
52+
return nodePath.dirname(cwd)
53+
}
54+
return cwd
55+
}),
56+
}
57+
})
58+
59+
// Mock i18n
60+
vi.mock("../../i18n", () => ({
61+
t: vi.fn((key: string, params?: any) => {
62+
// Return the key without namespace prefix to match actual behavior
63+
if (key.startsWith("common:")) {
64+
return key.replace("common:", "")
65+
}
66+
return key
67+
}),
68+
}))
69+
70+
describe("openFile", () => {
71+
beforeEach(() => {
72+
vi.clearAllMocks()
73+
vi.spyOn(console, "warn").mockImplementation(() => {})
74+
})
75+
76+
afterEach(() => {
77+
vi.restoreAllMocks()
78+
})
79+
80+
describe("decodeURIComponent error handling", () => {
81+
it("should handle invalid URI encoding gracefully", async () => {
82+
const invalidPath = "test%ZZinvalid.txt" // Invalid percent encoding
83+
const mockDocument = { uri: { fsPath: invalidPath } }
84+
85+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
86+
type: vscode.FileType.File,
87+
ctime: 0,
88+
mtime: 0,
89+
size: 0,
90+
})
91+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
92+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
93+
94+
await openFile(invalidPath)
95+
96+
// Should log a warning about decode failure
97+
expect(console.warn).toHaveBeenCalledWith(
98+
"Failed to decode file path: URIError: URI malformed. Using original path.",
99+
)
100+
101+
// Should still attempt to open the file with the original path
102+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
103+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
104+
})
105+
106+
it("should successfully decode valid URI-encoded paths", async () => {
107+
const encodedPath = "./%5Btest%5D/file.txt" // [test] encoded
108+
const decodedPath = "./[test]/file.txt"
109+
const mockDocument = { uri: { fsPath: decodedPath } }
110+
111+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
112+
type: vscode.FileType.File,
113+
ctime: 0,
114+
mtime: 0,
115+
size: 0,
116+
})
117+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
118+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
119+
120+
await openFile(encodedPath)
121+
122+
// Should not log any warnings
123+
expect(console.warn).not.toHaveBeenCalled()
124+
125+
// Should use the decoded path - verify it contains the decoded brackets
126+
// On Windows, the path will include backslashes instead of forward slashes
127+
const expectedPathSegment = process.platform === "win32" ? "[test]\\file.txt" : "[test]/file.txt"
128+
expect(vscode.Uri.file).toHaveBeenCalledWith(expect.stringContaining(expectedPathSegment))
129+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
130+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
131+
})
132+
133+
it("should handle paths with special characters that need encoding", async () => {
134+
const pathWithSpecialChars = "./[brackets]/file with spaces.txt"
135+
const mockDocument = { uri: { fsPath: pathWithSpecialChars } }
136+
137+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
138+
type: vscode.FileType.File,
139+
ctime: 0,
140+
mtime: 0,
141+
size: 0,
142+
})
143+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
144+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
145+
146+
await openFile(pathWithSpecialChars)
147+
148+
// Should work without errors
149+
expect(console.warn).not.toHaveBeenCalled()
150+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
151+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
152+
})
153+
154+
it("should handle already decoded paths without double-decoding", async () => {
155+
const normalPath = "./normal/file.txt"
156+
const mockDocument = { uri: { fsPath: normalPath } }
157+
158+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
159+
type: vscode.FileType.File,
160+
ctime: 0,
161+
mtime: 0,
162+
size: 0,
163+
})
164+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
165+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
166+
167+
await openFile(normalPath)
168+
169+
// Should work without errors
170+
expect(console.warn).not.toHaveBeenCalled()
171+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
172+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
173+
})
174+
})
175+
176+
describe("error handling", () => {
177+
it("should show error message when file does not exist", async () => {
178+
const nonExistentPath = "./does/not/exist.txt"
179+
180+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found"))
181+
182+
await openFile(nonExistentPath)
183+
184+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file")
185+
})
186+
187+
it("should handle generic errors", async () => {
188+
const testPath = "./test.txt"
189+
190+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue("Not an Error object")
191+
192+
await openFile(testPath)
193+
194+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file")
195+
})
196+
})
197+
198+
describe("directory handling", () => {
199+
it("should reveal directories in explorer", async () => {
200+
const dirPath = "./components"
201+
202+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
203+
type: vscode.FileType.Directory,
204+
ctime: 0,
205+
mtime: 0,
206+
size: 0,
207+
})
208+
209+
await openFile(dirPath)
210+
211+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
212+
"revealInExplorer",
213+
expect.objectContaining({ fsPath: expect.stringContaining("components") }),
214+
)
215+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("list.expand")
216+
expect(vscode.workspace.openTextDocument).not.toHaveBeenCalled()
217+
})
218+
})
219+
220+
describe("file creation", () => {
221+
it("should create new files when create option is true", async () => {
222+
const newFilePath = "./new/file.txt"
223+
const content = "Hello, world!"
224+
225+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found"))
226+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue({} as any)
227+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
228+
229+
await openFile(newFilePath, { create: true, content })
230+
231+
// On Windows, the path will include backslashes instead of forward slashes
232+
const expectedPathSegment = process.platform === "win32" ? "new\\file.txt" : "new/file.txt"
233+
expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(
234+
expect.objectContaining({ fsPath: expect.stringContaining(expectedPathSegment) }),
235+
Buffer.from(content, "utf8"),
236+
)
237+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
238+
})
239+
})
240+
})

src/integrations/misc/open-file.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@ interface OpenFileOptions {
1212

1313
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
1414
try {
15+
// Store the original path for error messages before any modifications
16+
const originalFilePathForError = filePath
17+
18+
// Try to decode the URI component, but if it fails, use the original path
19+
try {
20+
filePath = decodeURIComponent(filePath)
21+
} catch (decodeError) {
22+
// If decoding fails (e.g., invalid escape sequences), continue with the original path
23+
console.warn(`Failed to decode file path: ${decodeError}. Using original path.`)
24+
}
25+
1526
const workspaceRoot = getWorkspacePath()
1627
const homeDir = os.homedir()
17-
const originalFilePathForError = filePath // Keep original for error messages
1828

1929
const attemptPaths: string[] = []
2030

0 commit comments

Comments
 (0)