mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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:
parent
f5d7ba1959
commit
3ee0549c5d
4 changed files with 105 additions and 10 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue