From 3ee0549c5d0570329172c4c43f334384dd074ca2 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 26 Oct 2025 13:46:58 +0000 Subject: [PATCH] refactor: improve DOM manipulation pattern for UI picker triggers - Extract common logic into reusable triggerUIPicker helper function - Add error handling for missing DOM elements - Reduce code duplication in command handling - Improve maintainability and robustness - Fix ESLint dependency warning in handleKeyDown callback --- .../__tests__/built-in-commands.spec.ts | 10 ++--- src/services/command/built-in-commands.ts | 16 +++++++ .../src/components/chat/ChatTextArea.tsx | 45 ++++++++++++++++++- webview-ui/src/components/chat/TaskHeader.tsx | 44 ++++++++++++++++-- 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/services/command/__tests__/built-in-commands.spec.ts b/src/services/command/__tests__/built-in-commands.spec.ts index ecb2bdb0fb..cf43fde912 100644 --- a/src/services/command/__tests__/built-in-commands.spec.ts +++ b/src/services/command/__tests__/built-in-commands.spec.ts @@ -5,8 +5,8 @@ describe("Built-in Commands", () => { it("should return all built-in commands", async () => { const commands = await getBuiltInCommands() - expect(commands).toHaveLength(1) - expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init"])) + expect(commands).toHaveLength(3) + expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init", "profiles", "models"])) // Verify all commands have required properties commands.forEach((command) => { @@ -63,10 +63,10 @@ describe("Built-in Commands", () => { it("should return all built-in command names", async () => { const names = await getBuiltInCommandNames() - expect(names).toHaveLength(1) - expect(names).toEqual(expect.arrayContaining(["init"])) + expect(names).toHaveLength(3) + expect(names).toEqual(expect.arrayContaining(["init", "profiles", "models"])) // Order doesn't matter since it's based on filesystem order - expect(names.sort()).toEqual(["init"]) + expect(names.sort()).toEqual(["init", "models", "profiles"]) }) it("should return array of strings", async () => { diff --git a/src/services/command/built-in-commands.ts b/src/services/command/built-in-commands.ts index db113c4895..a7378ea2e8 100644 --- a/src/services/command/built-in-commands.ts +++ b/src/services/command/built-in-commands.ts @@ -8,6 +8,22 @@ interface BuiltInCommandDefinition { } const BUILT_IN_COMMANDS: Record = { + profiles: { + name: "profiles", + description: "Open the API configuration profile picker", + content: ` +This is a special command that opens the API configuration profile picker in the UI. +It does not execute a traditional slash command with text content. +`, + }, + models: { + name: "models", + description: "Open the model picker for the current API profile", + content: ` +This is a special command that opens the model picker in the UI. +It does not execute a traditional slash command with text content. +`, + }, init: { name: "init", description: "Analyze codebase and create concise AGENTS.md files for AI assistants", diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c7813372fa..fc06bad743 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -292,6 +292,27 @@ export const ChatTextArea = forwardRef( } }, [showContextMenu, setShowContextMenu]) + // Helper function to trigger UI pickers programmatically + const triggerUIPicker = useCallback( + (selector: string) => { + // Clear the input and close context menu + setSelectedMenuIndex(-1) + setInputValue("") + setShowContextMenu(false) + + // Find and click the picker trigger with error handling + setTimeout(() => { + const trigger = document.querySelector(selector) as HTMLElement + if (trigger) { + trigger.click() + } else { + console.warn(`Could not find UI element with selector: ${selector}`) + } + }, 100) + }, + [setInputValue], + ) + const handleMentionSelect = useCallback( (type: ContextMenuOptionType, value?: string) => { if (type === ContextMenuOptionType.NoResults) { @@ -308,7 +329,16 @@ export const ChatTextArea = forwardRef( } if (type === ContextMenuOptionType.Command && value) { - // Handle command selection. + // Handle special commands that trigger UI actions + if (value === "profiles") { + triggerUIPicker('[data-testid="dropdown-trigger"]') + return + } else if (value === "models") { + triggerUIPicker('[data-testid="mode-selector-trigger"]') + return + } + + // Handle regular command selection. setSelectedMenuIndex(-1) setInputValue("") setShowContextMenu(false) @@ -386,7 +416,7 @@ export const ChatTextArea = forwardRef( } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [setInputValue, cursorPosition], + [setInputValue, cursorPosition, triggerUIPicker], ) const handleKeyDown = useCallback( @@ -470,6 +500,16 @@ export const ChatTextArea = forwardRef( if (event.key === "Enter" && !event.shiftKey && !isComposing) { event.preventDefault() + // Check if the input is a special command that should trigger UI actions + const trimmedInput = inputValue.trim() + if (trimmedInput === "/profiles") { + triggerUIPicker('[data-testid="dropdown-trigger"]') + return + } else if (trimmedInput === "/models") { + triggerUIPicker('[data-testid="mode-selector-trigger"]') + return + } + // Always call onSend - let ChatView handle queueing when disabled resetHistoryNavigation() onSend() @@ -536,6 +576,7 @@ export const ChatTextArea = forwardRef( handleHistoryNavigation, resetHistoryNavigation, commands, + triggerUIPicker, ], ) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index aef0bc5eee..ec1b6741ea 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useRef, useState } from "react" +import { memo, useEffect, useRef, useState, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { useCloudUpsell } from "@src/hooks/useCloudUpsell" import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog" @@ -16,6 +16,7 @@ import { cn } from "@src/lib/utils" import { StandardTooltip } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" +import { vscode } from "@src/utils/vscode" import Thumbnails from "../common/Thumbnails" @@ -23,6 +24,7 @@ import { TaskActions } from "./TaskActions" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" import { TodoListDisplay } from "./TodoListDisplay" +import { ApiConfigSelector } from "./ApiConfigSelector" export interface TaskHeaderProps { task: ClineMessage @@ -50,7 +52,15 @@ const TaskHeader = ({ todos, }: TaskHeaderProps) => { const { t } = useTranslation() - const { apiConfiguration, currentTaskItem, clineMessages } = useExtensionState() + const { + apiConfiguration, + currentTaskItem, + clineMessages, + currentApiConfigName, + listApiConfigMeta, + pinnedApiConfigs, + togglePinnedApiConfig, + } = useExtensionState() const { id: modelId, info: model } = useSelectedModel(apiConfiguration) const [isTaskExpanded, setIsTaskExpanded] = useState(false) const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false) @@ -58,6 +68,20 @@ const TaskHeader = ({ autoOpenOnAuth: false, }) + // Find the ID and display text for the currently selected API configuration + const { currentConfigId, displayName } = useMemo(() => { + const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName) + return { + currentConfigId: currentConfig?.id || "", + displayName: currentApiConfigName || "", + } + }, [listApiConfigMeta, currentApiConfigName]) + + // Helper function to handle API config change + const handleApiConfigChange = useCallback((value: string) => { + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) + }, []) + // Check if the task is complete by looking at the last relevant message (skipping resume messages) const isTaskComplete = clineMessages && clineMessages.length > 0 @@ -151,7 +175,21 @@ const TaskHeader = ({ )} -
e.stopPropagation()}> +
e.stopPropagation()}> + {/* Add API Config Selector in header */} + {listApiConfigMeta && listApiConfigMeta.length > 0 && ( + + )}