Skip to content

Commit 3ee0549

Browse files
committed
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
1 parent f5d7ba1 commit 3ee0549

File tree

4 files changed

+105
-10
lines changed

4 files changed

+105
-10
lines changed

src/services/command/__tests__/built-in-commands.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ describe("Built-in Commands", () => {
55
it("should return all built-in commands", async () => {
66
const commands = await getBuiltInCommands()
77

8-
expect(commands).toHaveLength(1)
9-
expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init"]))
8+
expect(commands).toHaveLength(3)
9+
expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init", "profiles", "models"]))
1010

1111
// Verify all commands have required properties
1212
commands.forEach((command) => {
@@ -63,10 +63,10 @@ describe("Built-in Commands", () => {
6363
it("should return all built-in command names", async () => {
6464
const names = await getBuiltInCommandNames()
6565

66-
expect(names).toHaveLength(1)
67-
expect(names).toEqual(expect.arrayContaining(["init"]))
66+
expect(names).toHaveLength(3)
67+
expect(names).toEqual(expect.arrayContaining(["init", "profiles", "models"]))
6868
// Order doesn't matter since it's based on filesystem order
69-
expect(names.sort()).toEqual(["init"])
69+
expect(names.sort()).toEqual(["init", "models", "profiles"])
7070
})
7171

7272
it("should return array of strings", async () => {

src/services/command/built-in-commands.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ interface BuiltInCommandDefinition {
88
}
99

1010
const BUILT_IN_COMMANDS: Record<string, BuiltInCommandDefinition> = {
11+
profiles: {
12+
name: "profiles",
13+
description: "Open the API configuration profile picker",
14+
content: `<special_command>
15+
This is a special command that opens the API configuration profile picker in the UI.
16+
It does not execute a traditional slash command with text content.
17+
</special_command>`,
18+
},
19+
models: {
20+
name: "models",
21+
description: "Open the model picker for the current API profile",
22+
content: `<special_command>
23+
This is a special command that opens the model picker in the UI.
24+
It does not execute a traditional slash command with text content.
25+
</special_command>`,
26+
},
1127
init: {
1228
name: "init",
1329
description: "Analyze codebase and create concise AGENTS.md files for AI assistants",

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,27 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
292292
}
293293
}, [showContextMenu, setShowContextMenu])
294294

295+
// Helper function to trigger UI pickers programmatically
296+
const triggerUIPicker = useCallback(
297+
(selector: string) => {
298+
// Clear the input and close context menu
299+
setSelectedMenuIndex(-1)
300+
setInputValue("")
301+
setShowContextMenu(false)
302+
303+
// Find and click the picker trigger with error handling
304+
setTimeout(() => {
305+
const trigger = document.querySelector(selector) as HTMLElement
306+
if (trigger) {
307+
trigger.click()
308+
} else {
309+
console.warn(`Could not find UI element with selector: ${selector}`)
310+
}
311+
}, 100)
312+
},
313+
[setInputValue],
314+
)
315+
295316
const handleMentionSelect = useCallback(
296317
(type: ContextMenuOptionType, value?: string) => {
297318
if (type === ContextMenuOptionType.NoResults) {
@@ -308,7 +329,16 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
308329
}
309330

310331
if (type === ContextMenuOptionType.Command && value) {
311-
// Handle command selection.
332+
// Handle special commands that trigger UI actions
333+
if (value === "profiles") {
334+
triggerUIPicker('[data-testid="dropdown-trigger"]')
335+
return
336+
} else if (value === "models") {
337+
triggerUIPicker('[data-testid="mode-selector-trigger"]')
338+
return
339+
}
340+
341+
// Handle regular command selection.
312342
setSelectedMenuIndex(-1)
313343
setInputValue("")
314344
setShowContextMenu(false)
@@ -386,7 +416,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
386416
}
387417
},
388418
// eslint-disable-next-line react-hooks/exhaustive-deps
389-
[setInputValue, cursorPosition],
419+
[setInputValue, cursorPosition, triggerUIPicker],
390420
)
391421

392422
const handleKeyDown = useCallback(
@@ -470,6 +500,16 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
470500
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
471501
event.preventDefault()
472502

503+
// Check if the input is a special command that should trigger UI actions
504+
const trimmedInput = inputValue.trim()
505+
if (trimmedInput === "/profiles") {
506+
triggerUIPicker('[data-testid="dropdown-trigger"]')
507+
return
508+
} else if (trimmedInput === "/models") {
509+
triggerUIPicker('[data-testid="mode-selector-trigger"]')
510+
return
511+
}
512+
473513
// Always call onSend - let ChatView handle queueing when disabled
474514
resetHistoryNavigation()
475515
onSend()
@@ -536,6 +576,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
536576
handleHistoryNavigation,
537577
resetHistoryNavigation,
538578
commands,
579+
triggerUIPicker,
539580
],
540581
)
541582

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useEffect, useRef, useState } from "react"
1+
import { memo, useEffect, useRef, useState, useCallback, useMemo } from "react"
22
import { useTranslation } from "react-i18next"
33
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
44
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
@@ -16,13 +16,15 @@ import { cn } from "@src/lib/utils"
1616
import { StandardTooltip } from "@src/components/ui"
1717
import { useExtensionState } from "@src/context/ExtensionStateContext"
1818
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
19+
import { vscode } from "@src/utils/vscode"
1920

2021
import Thumbnails from "../common/Thumbnails"
2122

2223
import { TaskActions } from "./TaskActions"
2324
import { ContextWindowProgress } from "./ContextWindowProgress"
2425
import { Mention } from "./Mention"
2526
import { TodoListDisplay } from "./TodoListDisplay"
27+
import { ApiConfigSelector } from "./ApiConfigSelector"
2628

2729
export interface TaskHeaderProps {
2830
task: ClineMessage
@@ -50,14 +52,36 @@ const TaskHeader = ({
5052
todos,
5153
}: TaskHeaderProps) => {
5254
const { t } = useTranslation()
53-
const { apiConfiguration, currentTaskItem, clineMessages } = useExtensionState()
55+
const {
56+
apiConfiguration,
57+
currentTaskItem,
58+
clineMessages,
59+
currentApiConfigName,
60+
listApiConfigMeta,
61+
pinnedApiConfigs,
62+
togglePinnedApiConfig,
63+
} = useExtensionState()
5464
const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
5565
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
5666
const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
5767
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
5868
autoOpenOnAuth: false,
5969
})
6070

71+
// Find the ID and display text for the currently selected API configuration
72+
const { currentConfigId, displayName } = useMemo(() => {
73+
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
74+
return {
75+
currentConfigId: currentConfig?.id || "",
76+
displayName: currentApiConfigName || "",
77+
}
78+
}, [listApiConfigMeta, currentApiConfigName])
79+
80+
// Helper function to handle API config change
81+
const handleApiConfigChange = useCallback((value: string) => {
82+
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
83+
}, [])
84+
6185
// Check if the task is complete by looking at the last relevant message (skipping resume messages)
6286
const isTaskComplete =
6387
clineMessages && clineMessages.length > 0
@@ -151,7 +175,21 @@ const TaskHeader = ({
151175
</div>
152176
)}
153177
</div>
154-
<div className="flex items-center shrink-0 ml-2" onClick={(e) => e.stopPropagation()}>
178+
<div className="flex items-center shrink-0 ml-2 gap-1" onClick={(e) => e.stopPropagation()}>
179+
{/* Add API Config Selector in header */}
180+
{listApiConfigMeta && listApiConfigMeta.length > 0 && (
181+
<ApiConfigSelector
182+
value={currentConfigId}
183+
displayName={displayName}
184+
disabled={false}
185+
title={t("chat:selectApiConfig")}
186+
onChange={handleApiConfigChange}
187+
triggerClassName="text-xs min-w-[60px] max-w-[120px] text-ellipsis overflow-hidden"
188+
listApiConfigMeta={listApiConfigMeta}
189+
pinnedApiConfigs={pinnedApiConfigs}
190+
togglePinnedApiConfig={togglePinnedApiConfig}
191+
/>
192+
)}
155193
<StandardTooltip content={isTaskExpanded ? t("chat:task.collapse") : t("chat:task.expand")}>
156194
<button
157195
onClick={() => setIsTaskExpanded(!isTaskExpanded)}

0 commit comments

Comments
 (0)