diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 10384db8ede..7eec19ea6a7 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const +export const experimentIds = ["powerSteering", "multiFileApplyDiff", "showEnhancePromptButton"] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -19,6 +19,7 @@ export type ExperimentId = z.infer export const experimentsSchema = z.object({ powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), + showEnhancePromptButton: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 4a8f06d62a9..b36bf215303 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -23,11 +23,21 @@ describe("experiments", () => { }) }) + describe("SHOW_ENHANCE_PROMPT_BUTTON", () => { + it("is configured correctly", () => { + expect(EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON).toBe("showEnhancePromptButton") + expect(experimentConfigsMap.SHOW_ENHANCE_PROMPT_BUTTON).toMatchObject({ + enabled: true, + }) + }) + }) + describe("isEnabled", () => { it("returns false when POWER_STEERING experiment is not enabled", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + showEnhancePromptButton: true, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -36,6 +46,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: true, multiFileApplyDiff: false, + showEnhancePromptButton: true, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -44,8 +55,27 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + showEnhancePromptButton: true, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) + + it("returns true when SHOW_ENHANCE_PROMPT_BUTTON is enabled", () => { + const experiments: Record = { + powerSteering: false, + multiFileApplyDiff: false, + showEnhancePromptButton: true, + } + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON)).toBe(true) + }) + + it("returns false when SHOW_ENHANCE_PROMPT_BUTTON is disabled", () => { + const experiments: Record = { + powerSteering: false, + multiFileApplyDiff: false, + showEnhancePromptButton: false, + } + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON)).toBe(false) + }) }) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 1edadf654f1..4e9196311cd 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } fro export const EXPERIMENT_IDS = { MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff", POWER_STEERING: "powerSteering", + SHOW_ENHANCE_PROMPT_BUTTON: "showEnhancePromptButton", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -16,6 +17,7 @@ interface ExperimentConfig { export const experimentConfigsMap: Record = { MULTI_FILE_APPLY_DIFF: { enabled: false }, POWER_STEERING: { enabled: false }, + SHOW_ENHANCE_PROMPT_BUTTON: { enabled: true }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb2..69f3b953691 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -10,6 +10,7 @@ import { ExtensionMessage } from "@roo/ExtensionMessage" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" import { useAppTranslation } from "@/i18n/TranslationContext" +import { EXPERIMENT_IDS, experiments } from "@roo/experiments" import { ContextMenuOptionType, getContextMenuOptions, @@ -86,6 +87,7 @@ const ChatTextArea = forwardRef( togglePinnedApiConfig, taskHistory, clineMessages, + experiments: experimentsConfig, } = useExtensionState() // Find the ID and display text for the currently selected API configuration @@ -1109,29 +1111,31 @@ const ChatTextArea = forwardRef( onScroll={() => updateHighlights()} /> -
- - - -
+ {experiments.isEnabled(experimentsConfig, EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON) && ( +
+ + + +
+ )} {!isEditMode && (
diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index f53bab76a4c..60576cc16fa 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -73,6 +73,9 @@ describe("ChatTextArea", () => { }, taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) }) @@ -83,11 +86,50 @@ describe("ChatTextArea", () => { openedTabs: [], taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) render() const enhanceButton = getEnhancePromptButton() expect(enhanceButton).toHaveClass("cursor-not-allowed") }) + + it("should not be visible when experiment is disabled", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + taskHistory: [], + cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: false, + }, + }) + render() + + // The button should not exist in the DOM + const enhanceButton = screen.queryByRole("button", { + name: (_, element) => { + return element.querySelector(".lucide-wand-sparkles") !== null + }, + }) + expect(enhanceButton).not.toBeInTheDocument() + }) + + it("should be visible when experiment is enabled", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + taskHistory: [], + cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, + }) + render() + const enhanceButton = getEnhancePromptButton() + expect(enhanceButton).toBeInTheDocument() + }) }) describe("handleEnhancePrompt", () => { @@ -103,6 +145,9 @@ describe("ChatTextArea", () => { apiConfiguration, taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) render() @@ -125,6 +170,9 @@ describe("ChatTextArea", () => { }, taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) render() @@ -147,6 +195,9 @@ describe("ChatTextArea", () => { }, taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) render() @@ -174,6 +225,9 @@ describe("ChatTextArea", () => { }, taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) rerender() @@ -275,6 +329,9 @@ describe("ChatTextArea", () => { filePaths: [], openedTabs: [], cwd: mockCwd, + experiments: { + showEnhancePromptButton: true, + }, }) mockConvertToMentionPath.mockClear() }) @@ -506,6 +563,9 @@ describe("ChatTextArea", () => { taskHistory: [], clineMessages: mockClineMessages, cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) }) @@ -659,6 +719,9 @@ describe("ChatTextArea", () => { taskHistory: [], clineMessages: mixedClineMessages, cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) const setInputValue = vi.fn() @@ -687,6 +750,9 @@ describe("ChatTextArea", () => { taskHistory: [], clineMessages: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) const setInputValue = vi.fn() @@ -718,6 +784,9 @@ describe("ChatTextArea", () => { taskHistory: [], clineMessages: clineMessagesWithEmpty, cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) const setInputValue = vi.fn() @@ -752,6 +821,9 @@ describe("ChatTextArea", () => { taskHistory: mockTaskHistory, clineMessages: [], // No conversation messages cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) const setInputValue = vi.fn() @@ -789,6 +861,9 @@ describe("ChatTextArea", () => { ], clineMessages: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) rerender() @@ -812,6 +887,9 @@ describe("ChatTextArea", () => { { type: "say", say: "user_feedback", text: "Message 2", ts: 2000 }, ], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) setInputValue.mockClear() @@ -929,6 +1007,9 @@ describe("ChatTextArea", () => { cwd: "/test/workspace", customModes: [], customModePrompts: {}, + experiments: { + showEnhancePromptButton: true, + }, }) render() @@ -953,6 +1034,9 @@ describe("ChatTextArea", () => { openedTabs: [], taskHistory: [], cwd: "/test/workspace", + experiments: { + showEnhancePromptButton: true, + }, }) render() diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 53801232ecc..0c61ade9a26 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -37,31 +37,18 @@ export const ExperimentalSettings = ({
{Object.entries(experimentConfigsMap) - .filter(([key]) => key in EXPERIMENT_IDS) + .filter(([key]) => key in EXPERIMENT_IDS && key !== "SHOW_ENHANCE_PROMPT_BUTTON") .map((config) => { - if (config[0] === "MULTI_FILE_APPLY_DIFF") { - return ( - - setExperimentEnabled(EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF, enabled) - } - /> - ) - } + const experimentKey = config[0] as keyof typeof EXPERIMENT_IDS + const experimentId = EXPERIMENT_IDS[experimentKey] + const defaultEnabled = experimentConfigsMap[experimentKey].enabled + return ( - setExperimentEnabled( - EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], - enabled, - ) - } + key={experimentKey} + experimentKey={experimentKey} + enabled={experiments[experimentId] ?? defaultEnabled} + onChange={(enabled) => setExperimentEnabled(experimentId, enabled)} /> ) })} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index a416afd48de..afd1490cc7b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -23,6 +23,7 @@ import { Info, MessageSquare, LucideIcon, + Palette, } from "lucide-react" import type { ProviderSettings, ExperimentId } from "@roo-code/types" @@ -63,6 +64,7 @@ import { TerminalSettings } from "./TerminalSettings" import { ExperimentalSettings } from "./ExperimentalSettings" import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" +import { UISettings } from "./UISettings" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" import { cn } from "@/lib/utils" @@ -87,6 +89,7 @@ const sectionNames = [ "contextManagement", "terminal", "prompts", + "ui", "experimental", "language", "about", @@ -414,6 +417,7 @@ const SettingsView = forwardRef(({ onDone, t { id: "contextManagement", icon: Database }, { id: "terminal", icon: SquareTerminal }, { id: "prompts", icon: MessageSquare }, + { id: "ui", icon: Palette }, { id: "experimental", icon: FlaskConical }, { id: "language", icon: Globe }, { id: "about", icon: Info }, @@ -702,6 +706,11 @@ const SettingsView = forwardRef(({ onDone, t /> )} + {/* UI Section */} + {activeTab === "ui" && ( + + )} + {/* Experimental Section */} {activeTab === "experimental" && ( diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx new file mode 100644 index 00000000000..a829453915b --- /dev/null +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -0,0 +1,113 @@ +import { HTMLAttributes } from "react" +import { Palette } from "lucide-react" + +import type { Experiments } from "@roo-code/types" + +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { EXPERIMENT_IDS } from "@roo/experiments" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { cn } from "@src/lib/utils" + +import { SetExperimentEnabled } from "./types" +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +type UISettingsProps = HTMLAttributes & { + experiments: Experiments + setExperimentEnabled: SetExperimentEnabled +} + +export const UISettings = ({ experiments, setExperimentEnabled, className, ...props }: UISettingsProps) => { + const { t } = useAppTranslation() + + // For now, we'll only handle the enhance prompt button through experiments + // Other UI settings will be added later when backend support is implemented + const showEnhancePromptButton = experiments[EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON] ?? true + + return ( +
+ +
+ +
{t("settings:sections.ui")}
+
+
+ +
+
+ + setExperimentEnabled(EXPERIMENT_IDS.SHOW_ENHANCE_PROMPT_BUTTON, e.target.checked) + } + data-testid="show-enhance-prompt-button-checkbox"> + {t("settings:ui.showEnhancePromptButton.label")} + +
+ {t("settings:ui.showEnhancePromptButton.description")} +
+
+ +
+ + {t("settings:ui.showCodebaseIndexing.label")} + +
+ {t("settings:ui.showCodebaseIndexing.description")} +
+
+ +
+ + {t("settings:ui.showAddImagesToPrompt.label")} + +
+ {t("settings:ui.showAddImagesToPrompt.description")} +
+
+ +
+ + {t("settings:ui.showApiConfiguration.label")} + +
+ {t("settings:ui.showApiConfiguration.description")} +
+
+ +
+ + {t("settings:ui.showAutoApprove.label")} + +
+ {t("settings:ui.showAutoApprove.description")} +
+
+ +
+ + {t("settings:ui.showHelperText.label")} + +
+ {t("settings:ui.showHelperText.description")} +
+
+ +
+ + {t("settings:ui.showSendButton.label")} + +
+ {t("settings:ui.showSendButton.description")} +
+
+ +
+ + {t("settings:ui.comingSoon")} +
+
+
+ ) +} diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 1e5867d3fc3..770c4bb16ce 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -222,10 +222,8 @@ describe("mergeExtensionState", () => { apiConfiguration: { modelMaxThinkingTokens: 456, modelTemperature: 0.3 }, experiments: { powerSteering: true, - marketplace: false, - disableCompletionCommand: false, - concurrentFileReads: true, multiFileApplyDiff: true, + showEnhancePromptButton: true, } as Record, } @@ -238,10 +236,8 @@ describe("mergeExtensionState", () => { expect(result.experiments).toEqual({ powerSteering: true, - marketplace: false, - disableCompletionCommand: false, - concurrentFileReads: true, multiFileApplyDiff: true, + showEnhancePromptButton: true, }) }) }) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index cfd5b042868..bceb2354c16 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompts", "experimental": "Experimental", "language": "Language", - "about": "About Roo Code" + "about": "About Roo Code", + "ui": "UI" }, "prompts": { "description": "Configure support prompts that are used for quick actions like enhancing prompts, explaining code, and fixing issues. These prompts help Roo provide better assistance for common development tasks." @@ -648,8 +649,44 @@ "MULTI_FILE_APPLY_DIFF": { "name": "Enable concurrent file edits", "description": "When enabled, Roo can edit multiple files in a single request. When disabled, Roo must edit files one at a time. Disabling this can help when working with less capable models or when you want more control over file modifications." + }, + "SHOW_ENHANCE_PROMPT_BUTTON": { + "name": "Show enhance prompt button", + "description": "When enabled, displays the enhance prompt button in the chat input area. This button allows you to improve your prompts using AI assistance." } }, + "ui": { + "description": "Customize the user interface elements and visibility settings", + "showEnhancePromptButton": { + "label": "Show enhance prompt button", + "description": "Display the wand icon button in the chat input area that helps improve your prompts using AI assistance" + }, + "showCodebaseIndexing": { + "label": "Show codebase indexing status", + "description": "Display the codebase indexing status indicator in the chat interface" + }, + "showAddImagesToPrompt": { + "label": "Show add images button", + "description": "Display the image button that allows adding images to your prompts" + }, + "showApiConfiguration": { + "label": "Show API configuration selector", + "description": "Display the dropdown for selecting different API configurations" + }, + "showAutoApprove": { + "label": "Show auto-approve toggle", + "description": "Display the auto-approve toggle in the chat interface" + }, + "showHelperText": { + "label": "Show helper text", + "description": "Display the helper text '(@ to add context, / to switch modes...)' below the chat input" + }, + "showSendButton": { + "label": "Show send button", + "description": "Display the send button in the chat input area" + }, + "comingSoon": "Additional UI customization options are coming soon. The settings above are placeholders for future functionality." + }, "promptCaching": { "label": "Disable prompt caching", "description": "When checked, Roo will not use prompt caching for this model."