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
This commit is contained in:
Roo Code 2025-10-26 13:46:58 +00:00
parent f5d7ba1959
commit 3ee0549c5d
4 changed files with 105 additions and 10 deletions

View file

@ -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 () => {

View file

@ -8,6 +8,22 @@ interface BuiltInCommandDefinition {
}
const BUILT_IN_COMMANDS: Record<string, BuiltInCommandDefinition> = {
profiles: {
name: "profiles",
description: "Open the API configuration profile picker",
content: `<special_command>
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.
</special_command>`,
},
models: {
name: "models",
description: "Open the model picker for the current API profile",
content: `<special_command>
This is a special command that opens the model picker in the UI.
It does not execute a traditional slash command with text content.
</special_command>`,
},
init: {
name: "init",
description: "Analyze codebase and create concise AGENTS.md files for AI assistants",

View file

@ -292,6 +292,27 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [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<HTMLTextAreaElement, ChatTextAreaProps>(
}
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<HTMLTextAreaElement, ChatTextAreaProps>(
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setInputValue, cursorPosition],
[setInputValue, cursorPosition, triggerUIPicker],
)
const handleKeyDown = useCallback(
@ -470,6 +500,16 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
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<HTMLTextAreaElement, ChatTextAreaProps>(
handleHistoryNavigation,
resetHistoryNavigation,
commands,
triggerUIPicker,
],
)

View file

@ -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 = ({
</div>
)}
</div>
<div className="flex items-center shrink-0 ml-2" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center shrink-0 ml-2 gap-1" onClick={(e) => e.stopPropagation()}>
{/* Add API Config Selector in header */}
{listApiConfigMeta && listApiConfigMeta.length > 0 && (
<ApiConfigSelector
value={currentConfigId}
displayName={displayName}
disabled={false}
title={t("chat:selectApiConfig")}
onChange={handleApiConfigChange}
triggerClassName="text-xs min-w-[60px] max-w-[120px] text-ellipsis overflow-hidden"
listApiConfigMeta={listApiConfigMeta}
pinnedApiConfigs={pinnedApiConfigs}
togglePinnedApiConfig={togglePinnedApiConfig}
/>
)}
<StandardTooltip content={isTaskExpanded ? t("chat:task.collapse") : t("chat:task.expand")}>
<button
onClick={() => setIsTaskExpanded(!isTaskExpanded)}