Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/services/command/__tests__/built-in-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 () => {
Expand Down
16 changes: 16 additions & 0 deletions src/services/command/built-in-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 43 additions & 2 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"]')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /models command incorrectly triggers the mode selector instead of a model selector. The issue requirement states /models should "open the model picker scoped to the active profile" to allow users to swap AI models (like GPT-4, Claude, etc.). However, this implementation opens the ModeSelector component, which is for selecting operational modes (Code/Ask/Debug/etc.), not AI models. The [data-testid="mode-selector-trigger"] selector points to the ModeSelector component (see ModeSelector.tsx line 199), which serves a completely different purpose than selecting AI models.

return
}

// Handle regular command selection.
setSelectedMenuIndex(-1)
setInputValue("")
setShowContextMenu(false)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"]')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here - the /models command triggers the mode selector instead of a model selector. This duplicate occurrence needs the same fix as line 337.

return
}

// Always call onSend - let ChatView handle queueing when disabled
resetHistoryNavigation()
onSend()
Expand Down Expand Up @@ -536,6 +576,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
handleHistoryNavigation,
resetHistoryNavigation,
commands,
triggerUIPicker,
],
)

Expand Down
44 changes: 41 additions & 3 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -16,13 +16,15 @@ 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"

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
Expand Down Expand Up @@ -50,14 +52,36 @@ 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)
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
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
Expand Down Expand Up @@ -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)}
Expand Down