Skip to content

Commit b348333

Browse files
committed
feat(terminal): support selecting different profile
1 parent 7ba8e33 commit b348333

30 files changed

+730
-3
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const globalSettingsSchema = z.object({
121121
terminalZshP10k: z.boolean().optional(),
122122
terminalZdotdir: z.boolean().optional(),
123123
terminalCompressProgressBar: z.boolean().optional(),
124+
terminalPreferredProfile: z.string().optional(),
124125

125126
diagnosticsEnabled: z.boolean().optional(),
126127

src/core/webview/ClineProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ export class ClineProvider
738738
terminalZshP10k = false,
739739
terminalPowershellCounter = false,
740740
terminalZdotdir = false,
741+
terminalPreferredProfile,
741742
}) => {
742743
Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout)
743744
Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled)
@@ -747,6 +748,7 @@ export class ClineProvider
747748
Terminal.setTerminalZshP10k(terminalZshP10k)
748749
Terminal.setPowershellCounter(terminalPowershellCounter)
749750
Terminal.setTerminalZdotdir(terminalZdotdir)
751+
Terminal.setTerminalPreferredProfile(terminalPreferredProfile)
750752
},
751753
)
752754

@@ -1766,6 +1768,7 @@ export class ClineProvider
17661768
terminalZshOhMy,
17671769
terminalZshP10k,
17681770
terminalZdotdir,
1771+
terminalPreferredProfile,
17691772
fuzzyMatchThreshold,
17701773
mcpEnabled,
17711774
enableMcpServerCreation,
@@ -1892,6 +1895,7 @@ export class ClineProvider
18921895
terminalZshOhMy: terminalZshOhMy ?? false,
18931896
terminalZshP10k: terminalZshP10k ?? false,
18941897
terminalZdotdir: terminalZdotdir ?? false,
1898+
terminalPreferredProfile: terminalPreferredProfile ?? "",
18951899
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
18961900
mcpEnabled: mcpEnabled ?? true,
18971901
enableMcpServerCreation: enableMcpServerCreation ?? true,
@@ -2114,6 +2118,7 @@ export class ClineProvider
21142118
terminalZshP10k: stateValues.terminalZshP10k ?? false,
21152119
terminalZdotdir: stateValues.terminalZdotdir ?? false,
21162120
terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
2121+
terminalPreferredProfile: stateValues.terminalPreferredProfile ?? "",
21172122
mode: stateValues.mode ?? defaultModeSlug,
21182123
language: stateValues.language ?? formatLanguage(vscode.env.language),
21192124
mcpEnabled: stateValues.mcpEnabled ?? true,

src/core/webview/webviewMessageHandler.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import { checkExistKey } from "../../shared/checkExistApiConfig"
3636
import { experimentDefault } from "../../shared/experiments"
3737
import { Terminal } from "../../integrations/terminal/Terminal"
38+
import { TerminalProfileService } from "../../integrations/terminal/TerminalProfileService"
3839
import { openFile } from "../../integrations/misc/open-file"
3940
import { openImage, saveImage } from "../../integrations/misc/image-handler"
4041
import { selectImages } from "../../integrations/misc/process-images"
@@ -1461,6 +1462,31 @@ export const webviewMessageHandler = async (
14611462
Terminal.setCompressProgressBar(message.bool)
14621463
}
14631464
break
1465+
case "terminalPreferredProfile":
1466+
await updateGlobalState("terminalPreferredProfile", message.text)
1467+
await provider.postStateToWebview()
1468+
if (message.text !== undefined) {
1469+
Terminal.setTerminalPreferredProfile(message.text)
1470+
}
1471+
break
1472+
case "getTerminalProfiles":
1473+
try {
1474+
const profiles = TerminalProfileService.getAllSelectableProfiles()
1475+
await provider.postMessageToWebview({
1476+
type: "terminalProfiles",
1477+
profiles,
1478+
})
1479+
} catch (error) {
1480+
provider.log(
1481+
`Error getting terminal profiles: ${error instanceof Error ? error.message : String(error)}`,
1482+
)
1483+
// Send empty array on error
1484+
await provider.postMessageToWebview({
1485+
type: "terminalProfiles",
1486+
profiles: [],
1487+
})
1488+
}
1489+
break
14641490
case "mode":
14651491
await provider.handleModeSwitch(message.text as Mode)
14661492
break

src/integrations/terminal/BaseTerminal.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export abstract class BaseTerminal implements RooTerminal {
160160
private static terminalZshP10k: boolean = false
161161
private static terminalZdotdir: boolean = false
162162
private static compressProgressBar: boolean = true
163+
private static terminalPreferredProfile: string | undefined = undefined
163164

164165
/**
165166
* Compresses terminal output by applying run-length encoding and truncating to line limit
@@ -314,4 +315,20 @@ export abstract class BaseTerminal implements RooTerminal {
314315
public static getCompressProgressBar(): boolean {
315316
return BaseTerminal.compressProgressBar
316317
}
318+
319+
/**
320+
* Sets the preferred terminal profile
321+
* @param profileName The preferred terminal profile name
322+
*/
323+
public static setTerminalPreferredProfile(profileName: string | undefined): void {
324+
BaseTerminal.terminalPreferredProfile = profileName
325+
}
326+
327+
/**
328+
* Gets the preferred terminal profile
329+
* @returns The preferred terminal profile name
330+
*/
331+
public static getTerminalPreferredProfile(): string | undefined {
332+
return BaseTerminal.terminalPreferredProfile
333+
}
317334
}

src/integrations/terminal/Terminal.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { RooTerminalCallbacks, RooTerminalProcessResultPromise } from "./ty
55
import { BaseTerminal } from "./BaseTerminal"
66
import { TerminalProcess } from "./TerminalProcess"
77
import { ShellIntegrationManager } from "./ShellIntegrationManager"
8+
import { TerminalProfileService } from "./TerminalProfileService"
89
import { mergePromise } from "./mergePromise"
910

1011
export class Terminal extends BaseTerminal {
@@ -17,7 +18,34 @@ export class Terminal extends BaseTerminal {
1718

1819
const env = Terminal.getEnv()
1920
const iconPath = new vscode.ThemeIcon("rocket")
20-
this.terminal = terminal ?? vscode.window.createTerminal({ cwd, name: "Roo Code", iconPath, env })
21+
22+
// Get the full profile configuration from the user's preferred profile
23+
const preferredProfile = Terminal.getTerminalPreferredProfile()
24+
const profileOptions = TerminalProfileService.getTerminalOptionsForRoo(preferredProfile)
25+
26+
const terminalOptions: vscode.TerminalOptions = {
27+
cwd,
28+
name: "Roo Code",
29+
iconPath,
30+
env,
31+
}
32+
33+
// Apply profile configuration if available
34+
if (profileOptions) {
35+
// Merge profile options with our base options
36+
Object.assign(terminalOptions, profileOptions)
37+
38+
// Merge environment variables (profile env + our env)
39+
if (profileOptions.env) {
40+
terminalOptions.env = { ...profileOptions.env, ...env }
41+
}
42+
43+
// Keep our icon and name unless profile specifies otherwise
44+
terminalOptions.name = "Roo Code"
45+
terminalOptions.iconPath = iconPath
46+
}
47+
48+
this.terminal = terminal ?? vscode.window.createTerminal(terminalOptions)
2149

2250
if (Terminal.getTerminalZdotdir()) {
2351
ShellIntegrationManager.terminalTmpDirs.set(id, env.ZDOTDIR)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import * as vscode from "vscode"
2+
3+
export interface TerminalProfile {
4+
name: string
5+
path?: string
6+
args?: string[]
7+
icon?: string
8+
color?: string
9+
env?: Record<string, string>
10+
}
11+
12+
export interface TerminalProfileInfo {
13+
name: string
14+
displayName: string
15+
shellPath?: string
16+
isDefault?: boolean
17+
}
18+
19+
export class TerminalProfileService {
20+
/**
21+
* Gets the current platform identifier for terminal profiles
22+
*/
23+
private static getPlatform(): string {
24+
switch (process.platform) {
25+
case "win32":
26+
return "windows"
27+
case "darwin":
28+
return "osx"
29+
case "linux":
30+
return "linux"
31+
default:
32+
return "linux"
33+
}
34+
}
35+
36+
/**
37+
* Gets all available terminal profiles for the current platform
38+
*/
39+
public static getAvailableProfiles(): TerminalProfileInfo[] {
40+
const platform = this.getPlatform()
41+
const config = vscode.workspace.getConfiguration("terminal.integrated")
42+
43+
// Get profiles for the current platform
44+
const profiles = config.get<Record<string, TerminalProfile>>(`profiles.${platform}`) || {}
45+
46+
// Get the default profile name
47+
const defaultProfile = config.get<string>(`defaultProfile.${platform}`)
48+
49+
// Convert profiles to our format
50+
const profileInfos: TerminalProfileInfo[] = Object.entries(profiles).map(([name, profile]) => ({
51+
name,
52+
displayName: name,
53+
shellPath: profile.path,
54+
isDefault: name === defaultProfile,
55+
}))
56+
57+
// If no profiles are configured, return an empty array
58+
// VSCode will use its built-in defaults
59+
return profileInfos
60+
}
61+
62+
/**
63+
* Gets the default profile for the current platform
64+
*/
65+
public static getDefaultProfile(): TerminalProfileInfo | undefined {
66+
const platform = this.getPlatform()
67+
const config = vscode.workspace.getConfiguration("terminal.integrated")
68+
69+
const defaultProfileName = config.get<string>(`defaultProfile.${platform}`)
70+
71+
if (defaultProfileName) {
72+
const profiles = this.getAvailableProfiles()
73+
return profiles.find((profile) => profile.name === defaultProfileName)
74+
}
75+
76+
return undefined
77+
}
78+
79+
/**
80+
* Gets a specific profile by name
81+
*/
82+
public static getProfileByName(name: string): TerminalProfileInfo | undefined {
83+
const profiles = this.getAvailableProfiles()
84+
return profiles.find((profile) => profile.name === name)
85+
}
86+
87+
/**
88+
* Gets the shell path for a given profile name
89+
* Returns undefined if profile doesn't exist or doesn't specify a path
90+
*/
91+
public static getShellPathForProfile(profileName: string): string | undefined {
92+
if (!profileName) {
93+
return undefined
94+
}
95+
96+
// Check regular profiles
97+
const profile = this.getProfileByName(profileName)
98+
return profile?.shellPath
99+
}
100+
101+
/**
102+
* Gets the full profile configuration from terminal.integrated.profiles.{platform}
103+
*/
104+
public static getProfileConfiguration(profileName: string): Partial<vscode.TerminalOptions> | undefined {
105+
if (!profileName) {
106+
return undefined
107+
}
108+
109+
const platform = this.getPlatform()
110+
const config = vscode.workspace.getConfiguration("terminal.integrated")
111+
const profiles = config.get<Record<string, TerminalProfile>>(`profiles.${platform}`) || {}
112+
113+
const profile = profiles[profileName]
114+
if (!profile) {
115+
return undefined
116+
}
117+
118+
// Convert VSCode terminal profile to vscode.TerminalOptions
119+
const terminalOptions: Partial<vscode.TerminalOptions> = {}
120+
121+
if (profile.path) {
122+
terminalOptions.shellPath = profile.path
123+
}
124+
125+
if (profile.args) {
126+
terminalOptions.shellArgs = profile.args
127+
}
128+
129+
if (profile.env) {
130+
terminalOptions.env = profile.env
131+
}
132+
133+
if (profile.icon) {
134+
terminalOptions.iconPath = new vscode.ThemeIcon(profile.icon)
135+
}
136+
137+
if (profile.color) {
138+
terminalOptions.color = new vscode.ThemeColor(profile.color)
139+
}
140+
141+
return terminalOptions
142+
}
143+
144+
/**
145+
* Gets the terminal options that Roo should use for terminals
146+
* Priority: 1. User's preferred profile, 2. Default profile, 3. undefined (VSCode default)
147+
*/
148+
public static getTerminalOptionsForRoo(preferredProfile?: string): Partial<vscode.TerminalOptions> | undefined {
149+
// 1. Check user's preferred profile
150+
if (preferredProfile) {
151+
const profileConfig = this.getProfileConfiguration(preferredProfile)
152+
if (profileConfig) {
153+
return profileConfig
154+
}
155+
}
156+
157+
// 2. Check default profile
158+
const defaultProfile = this.getDefaultProfile()
159+
if (defaultProfile) {
160+
const profileConfig = this.getProfileConfiguration(defaultProfile.name)
161+
if (profileConfig) {
162+
return profileConfig
163+
}
164+
}
165+
166+
// 3. Let VSCode use its default
167+
return undefined
168+
}
169+
170+
/**
171+
* Gets the shell path that Roo should use for terminals (backward compatibility)
172+
* Priority: 1. User's preferred profile, 2. Default profile, 3. undefined (VSCode default)
173+
*/
174+
public static getShellPathForRoo(preferredProfile?: string): string | undefined {
175+
const terminalOptions = this.getTerminalOptionsForRoo(preferredProfile)
176+
return terminalOptions?.shellPath
177+
}
178+
179+
/**
180+
* Gets all profiles that should be shown in the UI
181+
*/
182+
public static getAllSelectableProfiles(): TerminalProfileInfo[] {
183+
const profiles = this.getAvailableProfiles()
184+
185+
// Add a "Default" option that represents using VSCode's default behavior
186+
profiles.unshift({
187+
name: "",
188+
displayName: "Default (VSCode Default)",
189+
isDefault: false,
190+
})
191+
192+
return profiles
193+
}
194+
}

0 commit comments

Comments
 (0)