Show API provider as popup

This commit is contained in:
Saoud Rizwan 2025-01-19 16:33:15 -08:00
parent 2b32940ca2
commit c50af7226c
6 changed files with 271 additions and 55 deletions

View file

@ -15,7 +15,6 @@ const AppContent = () => {
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [showAdvisorModelSettings, setShowAdvisorModelSettings] = useState(false)
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
@ -24,36 +23,26 @@ const AppContent = () => {
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowAdvisorModelSettings(false)
setShowHistory(false)
setShowMcp(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowAdvisorModelSettings(false)
setShowHistory(true)
setShowMcp(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowAdvisorModelSettings(false)
setShowHistory(false)
setShowMcp(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowAdvisorModelSettings(false)
setShowHistory(false)
setShowMcp(false)
break
}
break
case "openAdvisorModelSettings":
setShowSettings(true)
setShowAdvisorModelSettings(true)
setShowHistory(false)
setShowMcp(false)
break
}
}, [])
@ -76,9 +65,7 @@ const AppContent = () => {
<WelcomeView />
) : (
<>
{showSettings && (
<SettingsView onDone={() => setShowSettings(false)} showAdvisorModelSettings={showAdvisorModelSettings} />
)}
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}

View file

@ -793,7 +793,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => vscode.postMessage({ type: "openAdvisorModelSettings" })}>
in Settings.
in API Settings.
</VSCodeLink>
</div>
</>

View file

@ -14,8 +14,12 @@ import ContextMenu from "./ContextMenu"
import Thumbnails from "../common/Thumbnails"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { useWindowSize } from "react-use"
import { useEvent, useWindowSize } from "react-use"
import { vscode } from "../../utils/vscode"
import ApiOptions from "../settings/ApiOptions"
import { useClickAway } from "react-use"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
interface ChatTextAreaProps {
inputValue: string
@ -51,13 +55,11 @@ const SwitchContainer = styled.div<{ disabled: boolean }>`
border: 1px solid var(--vscode-input-border);
border-radius: 12px;
overflow: hidden;
position: absolute;
right: 15px;
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
transform: scale(0.85);
transform-origin: right center;
flex-shrink: 0;
margin-left: -10px; // compensate for the transform so flex spacing works
`
const Slider = styled.div<{ isChat: boolean }>`
@ -69,33 +71,117 @@ const Slider = styled.div<{ isChat: boolean }>`
transform: translateX(${(props) => (props.isChat ? "100%" : "0%")});
`
const ButtonGroup = styled.div`
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
`
const ButtonContainer = styled.div`
display: flex;
align-items: center;
gap: 3px;
font-size: 10px;
white-space: nowrap;
min-width: 0;
width: 100%;
`
const ACTUAL_SWITCH_WIDTH = 90
const SWITCH_WIDTH = ACTUAL_SWITCH_WIDTH * 0.85 // Account for the 0.85 scale transform
const CONTEXT_BUTTON_WIDTH = 60
const IMAGES_BUTTON_WIDTH = 80
const CONTAINER_PADDING = 30 // 15px left + 15px right
const TOTAL_WIDTH = SWITCH_WIDTH + 4 + CONTEXT_BUTTON_WIDTH + IMAGES_BUTTON_WIDTH + CONTAINER_PADDING
const ControlsContainer = styled.div`
display: flex;
align-items: center;
margin-top: -3px;
position: relative;
justify-content: space-between;
margin-top: -5px;
padding: 0px 15px 5px 15px;
`
const ButtonGroup = styled.div`
const ModelSelectorTooltip = styled.div<ModelSelectorTooltipProps>`
position: fixed;
bottom: calc(100% + 9px);
left: 15px;
right: 15px;
background: ${CODE_BLOCK_BG_COLOR};
border: 1px solid var(--vscode-editorGroup-border);
padding: 12px;
border-radius: 3px;
z-index: 1000;
max-height: calc(100vh - 100px);
overflow-y: auto;
overscroll-behavior: contain;
// Add invisible padding for hover zone
&::before {
content: "";
position: fixed;
bottom: ${(props) => `calc(100vh - ${props.menuPosition}px - 2px)`};
left: 0;
right: 0;
height: 8px;
}
// Arrow pointing down
&::after {
content: "";
position: fixed;
bottom: ${(props) => `calc(100vh - ${props.menuPosition}px)`};
right: ${(props) => props.arrowPosition}px;
width: 10px;
height: 10px;
background: ${CODE_BLOCK_BG_COLOR};
border-right: 1px solid var(--vscode-editorGroup-border);
border-bottom: 1px solid var(--vscode-editorGroup-border);
transform: rotate(45deg);
z-index: -1;
}
`
const ModelContainer = styled.div`
position: relative;
display: flex;
flex: 1;
min-width: 0;
`
const ModelDisplayButton = styled.a<{ isActive?: boolean }>`
padding: 0px 0px;
height: 20px;
width: 100%;
min-width: 0;
cursor: pointer;
text-decoration: ${(props) => (props.isActive ? "underline" : "none")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
outline: none;
user-select: none;
&:hover,
&:focus {
color: var(--vscode-foreground);
text-decoration: underline;
outline: none;
}
&:active {
color: var(--vscode-foreground);
text-decoration: underline;
outline: none;
}
&:focus-visible {
outline: none;
}
`
const ModelButtonContent = styled.div`
width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
@ -114,7 +200,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, chatSettings } = useExtensionState()
const { filePaths, chatSettings, apiConfiguration } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
@ -129,8 +215,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
const { width: windowWidth } = useWindowSize()
const showButtonText = windowWidth - CONTAINER_PADDING > TOTAL_WIDTH - CONTAINER_PADDING
const [showModelSelector, setShowModelSelector] = useState(false)
const [showModelSelectorWithAdvisor, setShowModelSelectorWithAdvisor] = useState(false)
const modelSelectorRef = useRef<HTMLDivElement>(null)
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
const buttonRef = useRef<HTMLDivElement>(null)
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
const queryItems = useMemo(() => {
return [
@ -538,6 +629,83 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
updateHighlights()
}, [inputValue, textAreaDisabled, handleInputChange, updateHighlights])
// Add click away handler
useClickAway(modelSelectorRef, () => {
setShowModelSelector(false)
})
// Get model display name
const modelDisplayName = useMemo(() => {
const unknownModel = "unknown"
if (!apiConfiguration) return unknownModel
switch (apiConfiguration.apiProvider) {
case "anthropic":
return `anthropic:${apiConfiguration.apiModelId || unknownModel}`
case "openai":
return `openai:${apiConfiguration.openAiModelId || unknownModel}`
case "openrouter":
return `openrouter:${apiConfiguration.openRouterModelId || unknownModel}`
case "bedrock":
return `bedrock:${apiConfiguration.apiModelId || unknownModel}`
case "vertex":
return `vertex:${apiConfiguration.apiModelId || unknownModel}`
case "ollama":
return `ollama:${apiConfiguration.ollamaModelId || unknownModel}`
case "lmstudio":
return `lmstudio:${apiConfiguration.lmStudioModelId || unknownModel}`
case "gemini":
return `gemini:${apiConfiguration.apiModelId || unknownModel}`
case "openai-native":
return `openai-native:${apiConfiguration.apiModelId || unknownModel}`
case "deepseek":
return `deepseek:${apiConfiguration.apiModelId || unknownModel}`
case "mistral":
return `mistral:${apiConfiguration.apiModelId || unknownModel}`
case "vscode-lm":
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
default:
return unknownModel
}
}, [apiConfiguration])
// Calculate arrow position and menu position based on button location
useEffect(() => {
if (showModelSelector && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect()
const buttonCenter = buttonRect.left + buttonRect.width / 2
// Calculate distance from right edge of viewport using viewport coordinates
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
setArrowPosition(rightPosition)
setMenuPosition(buttonRect.top + 1) // Added +1 to move menu down by 1px
}
}, [showModelSelector, viewportWidth, viewportHeight])
// Reset advisor settings when model selector is closed
useEffect(() => {
if (!showModelSelector) {
setShowModelSelectorWithAdvisor(false)
// Reset any active styling by blurring the button
const button = buttonRef.current?.querySelector("a")
if (button) {
button.blur()
}
}
}, [showModelSelector])
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "openAdvisorModelSettings":
setShowModelSelector(true)
setShowModelSelectorWithAdvisor(true)
break
}
}, [])
useEvent("message", handleMessage)
return (
<div>
<div
@ -723,10 +891,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px", marginTop: -1 }}>
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span style={{ fontSize: "13px", marginBottom: 2 }}>@</span>
{showButtonText && <span style={{ fontSize: "10px" }}>Context</span>}
<span style={{ fontSize: "13px", marginBottom: 1 }}>@</span>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
</ButtonContainer>
</VSCodeButton>
@ -739,17 +907,47 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onSelectImages()
}
}}
style={{
padding: "0px 0px",
height: "20px",
opacity: shouldDisableImages ? 0.5 : 1,
cursor: shouldDisableImages ? "not-allowed" : undefined,
}}>
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="codicon codicon-device-camera" style={{ fontSize: "14px" }} />
{showButtonText && <span style={{ fontSize: "10px" }}>Add images</span>}
<span className="codicon codicon-device-camera" style={{ fontSize: "14px", marginBottom: -3 }} />
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
</ButtonContainer>
</VSCodeButton>
<ModelContainer ref={modelSelectorRef}>
<div ref={buttonRef} style={{ width: "100%" }}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
onClick={() => setShowModelSelector(!showModelSelector)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
setShowModelSelector(!showModelSelector)
}
}}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</div>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
showAdvisorModelSettings={showModelSelectorWithAdvisor}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
advisorModelIdErrorMessage={undefined}
isPopup={true}
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
<SwitchContainer disabled={textAreaDisabled} onClick={onModeToggle}>
@ -763,4 +961,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
)
// Update TypeScript interface for styled-component props
interface ModelSelectorTooltipProps {
arrowPosition: number
menuPosition: number
}
export default ChatTextArea

View file

@ -50,6 +50,7 @@ interface ApiOptionsProps {
modelIdErrorMessage?: string
advisorModelIdErrorMessage?: string
showAdvisorModelSettings?: boolean
isPopup?: boolean
}
const TabPanel = ({ children, isSelected }: { children: React.ReactNode; isSelected: boolean }) => {
@ -88,12 +89,28 @@ const TabButton = ({
)
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
const DROPDOWN_Z_INDEX = 1001 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
const DropdownContainer = styled.div`
position: relative;
z-index: ${DROPDOWN_Z_INDEX};
// Force dropdowns to open downward
& vscode-dropdown::part(listbox) {
position: absolute !important;
top: 100% !important;
bottom: auto !important;
}
`
const ApiOptions = ({
showModelOptions,
apiErrorMessage,
modelIdErrorMessage,
advisorModelIdErrorMessage,
showAdvisorModelSettings,
isPopup,
}: ApiOptionsProps) => {
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
@ -190,8 +207,8 @@ const ApiOptions = ({
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
<DropdownContainer className="dropdown-container">
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
@ -202,7 +219,6 @@ const ApiOptions = ({
style={{
minWidth: 130,
position: "relative",
zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1,
}}>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
@ -217,7 +233,7 @@ const ApiOptions = ({
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
</VSCodeDropdown>
</div>
</DropdownContainer>
{selectedProvider === "anthropic" && (
<div>
@ -865,6 +881,7 @@ const ApiOptions = ({
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
</>
)}
@ -906,7 +923,9 @@ const ApiOptions = ({
{createDropdown(anthropicModels, "base")}
</div>
)}
{selectedProvider === "openrouter" && <OpenRouterModelPicker modelType="base" key="base-model-picker" />}
{selectedProvider === "openrouter" && (
<OpenRouterModelPicker modelType="base" key="base-model-picker" isPopup={isPopup} />
)}
{modelIdErrorMessage && (
<p
style={{
@ -935,7 +954,7 @@ const ApiOptions = ({
</div>
)}
{selectedProvider === "openrouter" && (
<OpenRouterModelPicker modelType="advisor" key="advisor-model-picker" />
<OpenRouterModelPicker modelType="advisor" key="advisor-model-picker" isPopup={isPopup} />
)}
{advisorModelIdErrorMessage && (
<p
@ -972,11 +991,13 @@ export const ModelInfoView = ({
modelInfo,
isDescriptionExpanded,
setIsDescriptionExpanded,
isPopup,
}: {
selectedModelId: string
modelInfo: ModelInfo
isDescriptionExpanded: boolean
setIsDescriptionExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}) => {
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
@ -987,6 +1008,7 @@ export const ModelInfoView = ({
markdown={modelInfo.description}
isExpanded={isDescriptionExpanded}
setIsExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
),
<ModelInfoSupportsItem

View file

@ -14,12 +14,14 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
export interface OpenRouterModelPickerProps {
modelType: ModelType
isPopup?: boolean
}
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ modelType }) => {
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ modelType, isPopup }) => {
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(
modelType === "advisor"
@ -230,6 +232,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ modelType
}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
) : (
<p
@ -360,11 +363,13 @@ export const ModelDescriptionMarkdown = memo(
key,
isExpanded,
setIsExpanded,
isPopup,
}: {
markdown?: string
key: string
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}) => {
const [reactContent, setMarkdown] = useRemark()
// const [isExpanded, setIsExpanded] = useState(false)
@ -434,7 +439,7 @@ export const ModelDescriptionMarkdown = memo(
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: "var(--vscode-sideBar-background)",
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more

View file

@ -8,11 +8,10 @@ import ApiOptions from "./ApiOptions"
const IS_DEV = false // FIXME: use flags when packaging
type SettingsViewProps = {
showAdvisorModelSettings: boolean
onDone: () => void
}
const SettingsView = ({ showAdvisorModelSettings, onDone }: SettingsViewProps) => {
const SettingsView = ({ onDone }: SettingsViewProps) => {
const { apiConfiguration, version, customInstructions, setCustomInstructions, openRouterModels } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@ -94,7 +93,6 @@ const SettingsView = ({ showAdvisorModelSettings, onDone }: SettingsViewProps) =
<div style={{ marginBottom: 5 }}>
<ApiOptions
showModelOptions={true}
showAdvisorModelSettings={showAdvisorModelSettings}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
advisorModelIdErrorMessage={advisorModelIdErrorMessage}