Improve UI experience

This commit is contained in:
System233 2025-02-20 06:09:39 +08:00
parent b732005d23
commit eeb1e45907
12 changed files with 303 additions and 354 deletions

View file

@ -15,6 +15,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.5",
@ -23,7 +24,7 @@
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.0.0",
"@vscode/webview-ui-toolkit": "^1.4.0",

View file

@ -4,6 +4,7 @@ import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
import { Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog"
import { Button, Input } from "../ui"
interface ApiConfigManagerProps {
currentApiConfigName?: string
@ -299,7 +300,7 @@ const ApiConfigManager = ({
aria-labelledby="new-profile-title">
<DialogContent className="p-4 max-w-sm">
<DialogTitle>New Configuration Profile</DialogTitle>
<VSCodeTextField
<Input
ref={newProfileInputRef}
value={newProfileName}
onInput={(e: unknown) => {
@ -324,15 +325,12 @@ const ApiConfigManager = ({
</p>
)}
<div className="flex justify-end gap-2 mt-4">
<VSCodeButton appearance="secondary" onClick={resetCreateState}>
<Button variant="secondary" onClick={resetCreateState}>
Cancel
</VSCodeButton>
<VSCodeButton
appearance="primary"
disabled={!newProfileName.trim()}
onClick={handleNewProfileSave}>
</Button>
<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
Create Profile
</VSCodeButton>
</Button>
</div>
</DialogContent>
</Dialog>

View file

@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useEvent } from "react-use"
import { memo, useCallback, useMemo, useState } from "react"
import { useDebounce, useEvent } from "react-use"
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { TemperatureControl } from "./TemperatureControl"
@ -88,29 +88,21 @@ const ApiOptions = ({
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const requestLocalModelsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// Pull ollama/lmstudio models
const requestLocalModels = useCallback(() => {
if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
}
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
// Debounced model updates, only executed 250ms after the user stops typing
useEffect(() => {
if (requestLocalModelsTimeoutRef.current) {
clearTimeout(requestLocalModelsTimeoutRef.current)
}
requestLocalModelsTimeoutRef.current = setTimeout(requestLocalModels, 250)
return () => {
if (requestLocalModelsTimeoutRef.current) {
clearTimeout(requestLocalModelsTimeoutRef.current)
useDebounce(
() => {
if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
}
}
}, [requestLocalModels])
},
250,
[selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl],
)
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
@ -663,8 +655,7 @@ const ApiOptions = ({
]}>
<div
style={{
padding: 15,
backgroundColor: "var(--vscode-editor-background)",
padding: 12,
}}>
<p
style={{
@ -678,24 +669,11 @@ const ApiOptions = ({
</p>
{/* Capabilities Section */}
<div
style={{
marginBottom: 20,
padding: 12,
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
borderRadius: 4,
}}>
<span
style={{
fontWeight: 500,
fontSize: "12px",
display: "block",
marginBottom: 12,
color: "var(--vscode-editor-foreground)",
}}>
<div>
<h3 className="font-medium text-sm text-vscode-editor-foreground">
Model Capabilities
</span>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
</h3>
<div className="flex flex-col gap-2">
<div className="token-config-field">
<VSCodeTextField
value={
@ -792,158 +770,104 @@ const ApiOptions = ({
</span>
</div>
</div>
</div>
</div>
<div
style={{
backgroundColor: "var(--vscode-editor-background)",
padding: "12px",
borderRadius: "4px",
marginTop: "8px",
border: "1px solid var(--vscode-input-border)",
transition: "background-color 0.2s ease",
}}>
<span
<div>
<h3 className="font-medium text-sm text-vscode-editor-foreground">Model Features</h3>
<div className="flex flex-col gap-2">
<div className="feature-toggle">
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox
checked={
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
openAiModelInfoSaneDefaults.supportsImages
}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
supportsImages: checked,
}
})}>
<span style={{ fontWeight: 500 }}>Image Support</span>
</Checkbox>
<i
className="codicon codicon-info"
title="Enable if the model can process and understand images in the input. Required for image-based assistance and visual code understanding."
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
cursor: "help",
}}
/>
</div>
<p
style={{
fontSize: "11px",
fontWeight: 500,
color: "var(--vscode-editor-foreground)",
display: "block",
marginBottom: "10px",
color: "var(--vscode-descriptionForeground)",
marginLeft: "24px",
marginTop: "4px",
lineHeight: "1.4",
marginBottom: 0,
}}>
Model Features
</span>
Allows the model to analyze and understand images, essential for visual code
assistance
</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<div className="feature-toggle">
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox
checked={
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
openAiModelInfoSaneDefaults.supportsImages
}
onChange={handleInputChange(
"openAiCustomModelInfo",
(checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
supportsImages: checked,
}
},
)}>
<span style={{ fontWeight: 500 }}>Image Support</span>
</Checkbox>
<i
className="codicon codicon-info"
title="Enable if the model can process and understand images in the input. Required for image-based assistance and visual code understanding."
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
cursor: "help",
}}
/>
</div>
<p
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
marginLeft: "24px",
marginTop: "4px",
lineHeight: "1.4",
}}>
Allows the model to analyze and understand images, essential for
visual code assistance
</p>
</div>
<div
className="feature-toggle"
<div
className="feature-toggle"
style={{
borderTop: "1px solid var(--vscode-input-border)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox
checked={
apiConfiguration?.openAiCustomModelInfo?.supportsComputerUse ??
false
}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
supportsComputerUse: checked,
}
})}>
<span style={{ fontWeight: 500 }}>Computer Use</span>
</Checkbox>
<i
className="codicon codicon-info"
title="Enable if the model can interact with your computer through commands and file operations. Required for automated tasks and file modifications."
style={{
borderTop: "1px solid var(--vscode-input-border)",
paddingTop: "12px",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox
checked={
apiConfiguration?.openAiCustomModelInfo
?.supportsComputerUse ?? false
}
onChange={handleInputChange(
"openAiCustomModelInfo",
(checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
supportsComputerUse: checked,
}
},
)}>
<span style={{ fontWeight: 500 }}>Computer Use</span>
</Checkbox>
<i
className="codicon codicon-info"
title="Enable if the model can interact with your computer through commands and file operations. Required for automated tasks and file modifications."
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
cursor: "help",
}}
/>
</div>
<p
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
marginLeft: "24px",
marginTop: "4px",
lineHeight: "1.4",
}}>
This model feature is for computer use like sonnet 3.5 support
</p>
</div>
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
cursor: "help",
}}
/>
</div>
<p
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
marginLeft: "24px",
marginTop: "4px",
lineHeight: "1.4",
marginBottom: 0,
}}>
This model feature is for computer use like sonnet 3.5 support
</p>
</div>
</div>
</div>
{/* Pricing Section */}
<div
style={{
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
padding: "12px",
borderRadius: "4px",
marginTop: "15px",
}}>
<div style={{ marginBottom: "12px" }}>
<span
style={{
fontWeight: 500,
fontSize: "12px",
color: "var(--vscode-editor-foreground)",
display: "block",
marginBottom: "4px",
}}>
Model Pricing
</span>
<span
style={{
fontSize: "11px",
color: "var(--vscode-descriptionForeground)",
display: "block",
}}>
Configure token-based pricing in USD per million tokens
</span>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
backgroundColor: "var(--vscode-editor-background)",
padding: "12px",
borderRadius: "4px",
}}>
<div>
<h3 className="font-medium text-sm text-vscode-editor-foreground mb-0">
Model Pricing
</h3>
<div className="text-xs">Configure token-based pricing in USD per million tokens</div>
<div className="flex flex-row gap-2 mt-1.5">
<div className="price-input">
<VSCodeTextField
value={

View file

@ -10,7 +10,16 @@ import ApiConfigManager from "./ApiConfigManager"
import { Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { ApiConfiguration } from "../../../../src/shared/api"
import ConfirmDialog from "../ui/comfirm-dialog"
import {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
AlertDialogHeader,
AlertDialogFooter,
} from "../ui/alert-dialog"
type SettingsViewProps = {
onDone: () => void
@ -223,13 +232,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
paddingBottom: "2px",
}
const sliderStyle = {
flexGrow: 1,
maxWidth: "80%",
accentColor: "var(--vscode-button-background)",
height: "2px",
}
return (
<div
style={{
@ -243,14 +245,21 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
flexDirection: "column",
overflow: "hidden",
}}>
<ConfirmDialog
icon="codicon-warning"
title="Unsaved changes"
message="Do you want to discard changes and continue?"
show={isDiscardDialogShow}
onResult={onConfirmDialogResult}
onClose={() => setDiscardDialogShow(false)}
aria-labelledby="unsave-warning-dialog"></ConfirmDialog>
<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
<AlertDialogDescription>
<span style={{ fontSize: "2em" }} className={`codicon codicon-warning align-middle mr-1`} />
Do you want to discard changes and continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<div
style={{
display: "flex",
@ -378,11 +387,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
step="100"
value={writeDelayMs}
onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
style={{
flex: 1,
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
</div>
@ -437,11 +442,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) =>
setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
}
style={{
flex: 1,
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
</div>
@ -535,30 +536,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{(allowedCommands ?? []).map((cmd, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
gap: "5px",
backgroundColor: "var(--vscode-button-secondaryBackground)",
padding: "2px 6px",
borderRadius: "4px",
border: "1px solid var(--vscode-button-secondaryBorder)",
height: "24px",
}}>
className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
<span>{cmd}</span>
<VSCodeButton
appearance="icon"
style={{
padding: 0,
margin: 0,
height: "20px",
width: "20px",
minWidth: "20px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--vscode-button-foreground)",
}}
className="text-primary-foreground"
onClick={() => {
const newCommands = (allowedCommands ?? []).filter(
(_, i) => i !== index,
@ -619,10 +601,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
max="100"
step="1"
value={screenshotQuality ?? 75}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
onChange={(e) => setCachedStateField("screenshotQuality", parseInt(e.target.value))}
style={{
...sliderStyle,
}}
/>
<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
</div>
@ -672,11 +652,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
step="0.01"
value={soundVolume ?? 0.5}
onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
style={{
flexGrow: 1,
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
aria-label="Volume"
/>
<span style={{ minWidth: "35px", textAlign: "left" }}>
@ -700,7 +676,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
step="1"
value={rateLimitSeconds}
onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
style={{ ...sliderStyle }}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
</div>
@ -722,7 +698,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) =>
setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
}
style={{ ...sliderStyle }}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
</div>
@ -746,7 +722,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) =>
setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))
}
style={{ ...sliderStyle }}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
</div>
@ -802,9 +778,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => {
setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
}}
style={{
...sliderStyle,
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>
{Math.round((fuzzyMatchThreshold || 1) * 100)}%

View file

@ -1,5 +1,6 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import { useDebounce } from "react-use"
interface TemperatureControlProps {
value: number | undefined
@ -9,13 +10,13 @@ interface TemperatureControlProps {
export const TemperatureControl = ({ value, onChange, maxValue = 1 }: TemperatureControlProps) => {
const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined)
const [inputValue, setInputValue] = useState(value?.toString() ?? "0")
const [inputValue, setInputValue] = useState(value)
useDebounce(() => onChange(inputValue), 50, [onChange, inputValue])
// Sync internal state with prop changes when switching profiles
useEffect(() => {
const hasCustomTemperature = value !== undefined
setIsCustomTemperature(hasCustomTemperature)
setInputValue(value?.toString() ?? "0")
setInputValue(value)
}, [value])
return (
@ -26,9 +27,9 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
const isChecked = e.target.checked
setIsCustomTemperature(isChecked)
if (!isChecked) {
onChange(undefined) // Unset the temperature
} else if (value !== undefined) {
onChange(value) // Use the value from apiConfiguration, if set
setInputValue(undefined) // Unset the temperature
} else {
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
}
}}>
<span style={{ fontWeight: "500" }}>Use custom temperature</span>
@ -48,27 +49,15 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
}}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<input
aria-label="Temperature control text input"
type="text"
type="range"
min="0"
max={maxValue}
step="0.01"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onBlur={(e) => {
const newValue = parseFloat(e.target.value)
if (!isNaN(newValue) && newValue >= 0 && newValue <= maxValue) {
onChange(newValue)
setInputValue(newValue.toString())
} else {
setInputValue(value?.toString() ?? "0") // Reset to last valid value
}
}}
style={{
width: "60px",
padding: "4px 8px",
border: "1px solid var(--vscode-input-border)",
background: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
onChange={(e) => setInputValue(parseFloat(e.target.value))}
/>
<span>{inputValue}</span>
</div>
<p style={{ fontSize: "12px", marginTop: "8px", color: "var(--vscode-descriptionForeground)" }}>
Higher values make output more random, lower values make it more deterministic.

View file

@ -18,12 +18,12 @@ describe("TemperatureControl", () => {
const checkbox = screen.getByRole("checkbox")
expect(checkbox).toBeChecked()
const input = screen.getByRole("textbox")
const input = screen.getByRole("slider")
expect(input).toBeInTheDocument()
expect(input).toHaveValue("0.7")
})
it("updates when checkbox is toggled", () => {
it("updates when checkbox is toggled", async () => {
const onChange = jest.fn()
render(<TemperatureControl value={0.7} onChange={onChange} />)
@ -31,40 +31,50 @@ describe("TemperatureControl", () => {
// Uncheck - should clear temperature
fireEvent.click(checkbox)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(undefined)
// Check - should restore previous temperature
fireEvent.click(checkbox)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(0.7)
})
it("updates temperature when input loses focus", () => {
it("updates temperature when input loses focus", async () => {
const onChange = jest.fn()
render(<TemperatureControl value={0.7} onChange={onChange} />)
const input = screen.getByRole("textbox")
const input = screen.getByRole("slider")
fireEvent.change(input, { target: { value: "0.8" } })
fireEvent.blur(input)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(0.8)
})
it("respects maxValue prop", () => {
it("respects maxValue prop", async () => {
const onChange = jest.fn()
render(<TemperatureControl value={1.5} onChange={onChange} maxValue={2} />)
const input = screen.getByRole("textbox")
const input = screen.getByRole("slider")
// Valid value within max
fireEvent.change(input, { target: { value: "1.8" } })
fireEvent.blur(input)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(1.8)
// Invalid value above max
fireEvent.change(input, { target: { value: "2.5" } })
fireEvent.blur(input)
expect(input).toHaveValue("1.5") // Should revert to original value
expect(onChange).toHaveBeenCalledTimes(1) // Should not call onChange for invalid value
expect(input).toHaveValue("2") // Clamped between 0 and 2
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(2)
})
it("syncs checkbox state when value prop changes", () => {

View file

@ -0,0 +1,108 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-row justify-end space-x-2", className)} {...props} />
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-base text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "secondary" }), "mt-0", className)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View file

@ -5,19 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer active:opacity-90",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "text-primary-foreground bg-primary shadow hover:bg-primary/90",
secondary: "text-secondary-foreground bg-secondary shadow-sm hover:bg-secondary/80",
default:
"border border-vscode-input-border bg-primary text-primary-foreground shadow hover:bg-primary/90 cursor-pointer",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 cursor-pointer",
outline:
"text-secondary-foreground bg-vscode-editor-background border border-vscode-dropdown-border shadow-sm hover:bg-vscode-editor-background/50",
ghost: "text-secondary-foreground hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
destructive: "text-destructive-foreground bg-destructive shadow-sm hover:bg-destructive/90",
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground cursor-pointer",
secondary:
"border border-vscode-input-border bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 cursor-pointer",
ghost: "hover:bg-accent hover:text-accent-foreground cursor-pointer",
link: "text-primary underline-offset-4 hover:underline cursor-pointer",
combobox:
"text-secondary-foreground bg-vscode-input-background border border-vscode-input-border hover:bg-vscode-input-background/80",
"text-vscode-font-size font-normal text-popover-foreground bg-vscode-input-background border border-vscode-dropdown-border hover:bg-vscode-input-background/80 cursor-pointer",
},
size: {
default: "h-7 px-3",

View file

@ -1,58 +0,0 @@
import { Dialog, DialogContent, DialogTitle } from "./dialog"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback } from "react"
export interface ConfirmDialogProps {
show: boolean
icon: string
title?: string
message: string
onResult: (confirm: boolean) => void
onClose: () => void
}
export const ConfirmDialog = ({ onResult, onClose, icon, show, title, message }: ConfirmDialogProps) => {
const onCloseConfirmDialog = useCallback(
(confirm: boolean) => {
onResult(confirm)
onClose()
},
[onClose, onResult],
)
return (
<Dialog
open={show}
onOpenChange={(open) => {
!open && onCloseConfirmDialog(false)
}}
aria-labelledby="unsave-warning-dialog">
<DialogContent className="p-4 max-w-sm">
<DialogTitle>{title}</DialogTitle>
<p className="text-lg mt-2" data-testid="error-message">
<span
style={{ fontSize: "2em" }}
className={`codicon align-middle mr-1 ${icon || "codicon-warning"}`}
/>
<span>{message}</span>
</p>
<div className="flex justify-end gap-2 mt-4">
<VSCodeButton
appearance="primary"
onClick={() => {
onCloseConfirmDialog(true)
}}>
Yes
</VSCodeButton>
<VSCodeButton
appearance="secondary"
onClick={() => {
onCloseConfirmDialog(false)
}}>
No
</VSCodeButton>
</div>
</DialogContent>
</Dialog>
)
}
export default ConfirmDialog

View file

@ -43,7 +43,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md bg-transparent py-3 text-base outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@ -108,7 +108,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-pointer gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm text-vscode-dropdown-foreground outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-pointer gap-2 select-none items-center rounded-sm px-2 py-1.5 text-base text-vscode-dropdown-foreground outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className,
)}
{...props}>

View file

@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex w-full text-vscode-input-foreground border border-vscode-input-border bg-vscode-input-background rounded-xs px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
"flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}