Skip to content

Commit 543fb2a

Browse files
committed
Merge branch 'main' into cte/monorepo
2 parents 1b4fefc + 1b37e30 commit 543fb2a

File tree

9 files changed

+185
-23
lines changed

9 files changed

+185
-23
lines changed

CHANGELOG.md

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

3+
## [3.16.4] - 2025-05-09
4+
5+
- Improve provider profile management in the external API
6+
- Enforce provider selection in OpenRouter by using 'only' parameter and disabling fallbacks (thanks @shariqriazz!)
7+
- Fix display issues with long profile names (thanks @cannuri!)
8+
- Prevent terminal focus theft on paste after command execution (thanks @MuriloFP!)
9+
- Save OpenAI compatible custom headers correctly
10+
- Fix race condition when updating prompts (thanks @elianiva!)
11+
- Fix display issues in high contrast themes (thanks @zhangtony239!)
12+
- Fix not being able to use specific providers on Openrouter (thanks @daniel-lxs!)
13+
- Show properly formatted multi-line commands in preview (thanks @KJ7LNW!)
14+
- Handle unsupported language errors gracefully in read_file tool (thanks @KJ7LNW!)
15+
- Enhance focus styles in select-dropdown and fix docs URL (thanks @zhangtony239!)
16+
- Properly handle mode name overflow in UI (thanks @elianiva!)
17+
- Fix project MCP always allow issue (thanks @aheizi!)
18+
319
## [3.16.3] - 2025-05-08
420

521
- Revert Tailwind migration while we fix a few spots

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
55
"publisher": "RooVeterinaryInc",
6-
"version": "3.16.3",
6+
"version": "3.16.4",
77
"icon": "assets/icons/icon.png",
88
"galleryBanner": {
99
"color": "#617A91",

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
824824
"py-2",
825825
"px-[9px]",
826826
"z-10",
827+
"forced-color-adjust-none",
827828
)}
828829
style={{
829830
color: "transparent",

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
2+
import { convertHeadersToObject } from "./utils/headers"
23
import { useDebounce } from "react-use"
34
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
45

@@ -86,25 +87,6 @@ const ApiOptions = ({
8687
}, [apiConfiguration?.openAiHeaders, customHeaders])
8788

8889
// Helper to convert array of tuples to object (filtering out empty keys).
89-
const convertHeadersToObject = (headers: [string, string][]): Record<string, string> => {
90-
const result: Record<string, string> = {}
91-
92-
// Process each header tuple.
93-
for (const [key, value] of headers) {
94-
const trimmedKey = key.trim()
95-
96-
// Skip empty keys.
97-
if (!trimmedKey) {
98-
continue
99-
}
100-
101-
// For duplicates, the last one in the array wins.
102-
// This matches how HTTP headers work in general.
103-
result[trimmedKey] = value.trim()
104-
}
105-
106-
return result
107-
}
10890

10991
// Debounced effect to update the main configuration when local
11092
// customHeaders state stabilizes.

webview-ui/src/components/settings/providers/OpenAICompatible.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useState, useCallback } from "react"
1+
import { useState, useCallback, useEffect } from "react"
22
import { useEvent } from "react-use"
33
import { Checkbox } from "vscrui"
44
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
5+
import { convertHeadersToObject } from "../utils/headers"
56

67
import { ModelInfo, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
78
import { ProviderSettings, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
@@ -67,6 +68,18 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
6768
setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
6869
}, [])
6970

71+
// Helper to convert array of tuples to object
72+
73+
// Add effect to update the parent component's state when local headers change
74+
useEffect(() => {
75+
const timer = setTimeout(() => {
76+
const headerObject = convertHeadersToObject(customHeaders)
77+
setApiConfigurationField("openAiHeaders", headerObject)
78+
}, 300)
79+
80+
return () => clearTimeout(timer)
81+
}, [customHeaders, setApiConfigurationField])
82+
7083
const handleInputChange = useCallback(
7184
<K extends keyof ProviderSettings, E>(
7285
field: K,
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { convertHeadersToObject } from "../headers"
2+
3+
describe("convertHeadersToObject", () => {
4+
it("should convert headers array to object", () => {
5+
const headers: [string, string][] = [
6+
["Content-Type", "application/json"],
7+
["Authorization", "Bearer token123"],
8+
]
9+
10+
const result = convertHeadersToObject(headers)
11+
12+
expect(result).toEqual({
13+
"Content-Type": "application/json",
14+
Authorization: "Bearer token123",
15+
})
16+
})
17+
18+
it("should trim whitespace from keys and values", () => {
19+
const headers: [string, string][] = [
20+
[" Content-Type ", " application/json "],
21+
[" Authorization ", " Bearer token123 "],
22+
]
23+
24+
const result = convertHeadersToObject(headers)
25+
26+
expect(result).toEqual({
27+
"Content-Type": "application/json",
28+
Authorization: "Bearer token123",
29+
})
30+
})
31+
32+
it("should handle empty headers array", () => {
33+
const headers: [string, string][] = []
34+
35+
const result = convertHeadersToObject(headers)
36+
37+
expect(result).toEqual({})
38+
})
39+
40+
it("should skip headers with empty keys", () => {
41+
const headers: [string, string][] = [
42+
["Content-Type", "application/json"],
43+
["", "This value should be skipped"],
44+
[" ", "This value should also be skipped"],
45+
["Authorization", "Bearer token123"],
46+
]
47+
48+
const result = convertHeadersToObject(headers)
49+
50+
expect(result).toEqual({
51+
"Content-Type": "application/json",
52+
Authorization: "Bearer token123",
53+
})
54+
55+
// Specifically verify empty keys are not present
56+
expect(result[""]).toBeUndefined()
57+
expect(result[" "]).toBeUndefined()
58+
})
59+
60+
it("should use last occurrence when handling duplicate keys", () => {
61+
const headers: [string, string][] = [
62+
["Content-Type", "application/json"],
63+
["Authorization", "Bearer token123"],
64+
["Content-Type", "text/plain"], // Duplicate key - should override previous value
65+
["Content-Type", "application/xml"], // Another duplicate - should override again
66+
]
67+
68+
const result = convertHeadersToObject(headers)
69+
70+
// Verify the last value for "Content-Type" is used
71+
expect(result["Content-Type"]).toBe("application/xml")
72+
expect(result).toEqual({
73+
"Content-Type": "application/xml",
74+
Authorization: "Bearer token123",
75+
})
76+
})
77+
78+
it("should preserve case sensitivity while trimming keys", () => {
79+
const headers: [string, string][] = [
80+
[" Content-Type", "application/json"],
81+
["content-type ", "text/plain"], // Different casing (lowercase) with spacing
82+
]
83+
84+
const result = convertHeadersToObject(headers)
85+
86+
// Keys should be trimmed but case sensitivity preserved
87+
// JavaScript object keys are case-sensitive
88+
expect(Object.keys(result)).toHaveLength(2)
89+
expect(result["Content-Type"]).toBe("application/json")
90+
expect(result["content-type"]).toBe("text/plain")
91+
})
92+
93+
it("should handle empty values", () => {
94+
const headers: [string, string][] = [
95+
["Empty-Value", ""],
96+
["Whitespace-Value", " "],
97+
]
98+
99+
const result = convertHeadersToObject(headers)
100+
101+
// Empty values should be included but trimmed
102+
expect(result["Empty-Value"]).toBe("")
103+
expect(result["Whitespace-Value"]).toBe("")
104+
})
105+
106+
it("should handle complex duplicate key scenarios with mixed casing and spacing", () => {
107+
const headers: [string, string][] = [
108+
["content-type", "application/json"], // Original entry
109+
[" Content-Type ", "text/html"], // Different case with spacing
110+
["content-type", "application/xml"], // Same case as first, should override it
111+
["Content-Type", "text/plain"], // Same case as second, should override it
112+
]
113+
114+
const result = convertHeadersToObject(headers)
115+
116+
// JavaScript object keys are case-sensitive
117+
// We should have two keys with different cases, each with the last value
118+
expect(Object.keys(result).sort()).toEqual(["Content-Type", "content-type"].sort())
119+
expect(result["content-type"]).toBe("application/xml")
120+
expect(result["Content-Type"]).toBe("text/plain")
121+
})
122+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Converts an array of header key-value pairs to a Record object.
3+
*
4+
* @param headers Array of [key, value] tuples representing HTTP headers
5+
* @returns Record with trimmed keys and values
6+
*/
7+
export const convertHeadersToObject = (headers: [string, string][]): Record<string, string> => {
8+
const result: Record<string, string> = {}
9+
10+
// Process each header tuple.
11+
for (const [key, value] of headers) {
12+
const trimmedKey = key.trim()
13+
14+
// Skip empty keys.
15+
if (!trimmedKey) {
16+
continue
17+
}
18+
19+
// For duplicates, the last one in the array wins.
20+
// This matches how HTTP headers work in general.
21+
result[trimmedKey] = value.trim()
22+
}
23+
24+
return result
25+
}

webview-ui/src/components/welcome/RooHero.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const RooHero = () => {
77
})
88

99
return (
10-
<div className="flex flex-col items-center justify-center pb-4">
10+
<div className="flex flex-col items-center justify-center pb-4 forced-color-adjust-none">
1111
<div
1212
style={{
1313
backgroundColor: "var(--vscode-foreground)",

webview-ui/src/components/welcome/RooTips.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ const RooTips = ({ cycle = false }: RooTipsProps) => {
7474
className="flex items-center gap-2 text-vscode-editor-foreground font-vscode max-w-[250px]">
7575
<span className={`codicon ${tip.icon}`}></span>
7676
<span>
77-
<VSCodeLink href={tip.href}>{t(tip.titleKey)}</VSCodeLink>: {t(tip.descriptionKey)}
77+
<VSCodeLink className="forced-color-adjust-none" href={tip.href}>
78+
{t(tip.titleKey)}
79+
</VSCodeLink>
80+
: {t(tip.descriptionKey)}
7881
</span>
7982
</div>
8083
))

0 commit comments

Comments
 (0)