feat(vscode): improve custom provider dialog layout and model toggles

This commit is contained in:
Christiaan Arnoldus
2026-08-10 15:17:17 +02:00
parent 95ad1705f5
commit 981600921a
26 changed files with 207 additions and 79 deletions
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Improve custom provider edit dialog layout, make advanced configuration action prominent, and add bulk toggle buttons for reasoning and image modalities across all models.
@@ -12,6 +12,24 @@
border: 1px solid var(--border-weak-base);
}
&[data-size="large"] [data-slot="dialog-container"] {
width: min(calc(100vw - 32px), 800px);
height: min(calc(100vh - 32px), 720px);
}
&[data-size="large"] [data-slot="dialog-content"] {
max-width: 800px;
}
&[data-size="x-large"] [data-slot="dialog-container"] {
width: min(calc(100vw - 32px), 980px);
height: min(calc(100vh - 32px), 800px);
}
&[data-size="x-large"] [data-slot="dialog-content"] {
max-width: 980px;
}
[data-slot="dialog-header"] {
font-size: var(--kilo-font-size-14);
}
@@ -292,4 +292,20 @@ describe("validateCustomProvider variant name validation", () => {
const saved = out.result!.config.models["model-1"] as Record<string, unknown>
expect(saved.modalities).toEqual({ input: ["text", "audio", "video", "pdf"], output: ["text", "audio"] })
})
it("handles multiple models with reasoning and images toggled", () => {
const form = base()
form.models = [
{ id: "m1", name: "Model 1", reasoning: true, supportsImages: true, modalities: {}, variants: [] },
{ id: "m2", name: "Model 2", reasoning: true, supportsImages: false, modalities: {}, variants: [] },
]
const out = validateCustomProvider(args(form))
expect(out.result).toBeDefined()
const m1 = out.result!.config.models["m1"] as Record<string, unknown>
const m2 = out.result!.config.models["m2"] as Record<string, unknown>
expect(m1.reasoning).toBe(true)
expect(m1.modalities).toEqual({ input: ["text", "image"] })
expect(m2.reasoning).toBe(true)
expect(m2.modalities).toBeUndefined()
})
})
@@ -451,6 +451,18 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
setErrors("models", (v) => v.filter((_, i) => i !== index))
}
function toggleAllReasoning() {
const all = form.models.length > 0 && form.models.every((m) => m.reasoning)
const target = !all
form.models.forEach((_, i) => setForm("models", i, "reasoning", target))
}
function toggleAllImages() {
const all = form.models.length > 0 && form.models.every((m) => m.supportsImages)
const target = !all
form.models.forEach((_, i) => setForm("models", i, "supportsImages", target))
}
function addHeader() {
setForm("headers", (v) => [...v, { key: "", value: "" }])
setErrors("headers", (v) => [...v, {}])
@@ -515,6 +527,7 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
return (
<Dialog
size="large"
title={
<IconButton
tabIndex={-1}
@@ -530,13 +543,15 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
style={{
display: "flex",
"flex-direction": "column",
gap: "24px",
padding: "0 10px 12px 10px",
gap: "20px",
padding: "0 16px 16px 16px",
"overflow-y": "auto",
"max-height": "60vh",
flex: 1,
width: "100%",
"box-sizing": "border-box",
}}
>
<div style={{ padding: "0 10px", display: "flex", gap: "16px", "align-items": "center" }}>
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
<ProviderIcon id="synthetic" width={20} height={20} />
<div
style={{ "font-size": "var(--kilo-font-size-16)", "font-weight": "500", color: "var(--vscode-foreground)" }}
@@ -545,36 +560,35 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
</div>
</div>
<form
onSubmit={save}
style={{ padding: "0 10px 24px 10px", display: "flex", "flex-direction": "column", gap: "24px" }}
>
<div style={{ "font-size": "var(--kilo-font-size-14)", color: "var(--text-base)" }}>
{language.t("provider.custom.description.prefix")}
<a
href="https://kilo.ai/docs/ai-providers#custom-provider"
onClick={(e) => {
e.preventDefault()
vscode.postMessage({
type: "openExternal",
url: "https://kilo.ai/docs/ai-providers#custom-provider",
})
}}
>
{language.t("provider.custom.description.link")}
</a>
{language.t("provider.custom.description.suffix")}
<form onSubmit={save} style={{ display: "flex", "flex-direction": "column", gap: "20px" }}>
<div style={{ display: "flex", "flex-direction": "column", gap: "10px" }}>
<div style={{ "font-size": "var(--kilo-font-size-14)", color: "var(--text-base)" }}>
{language.t("provider.custom.description.prefix")}
<a
href="https://kilo.ai/docs/ai-providers#custom-provider"
onClick={(e) => {
e.preventDefault()
vscode.postMessage({
type: "openExternal",
url: "https://kilo.ai/docs/ai-providers#custom-provider",
})
}}
>
{language.t("provider.custom.description.link")}
</a>
{language.t("provider.custom.description.suffix")}
</div>
<Show when={editing()}>
<div style={{ "margin-top": "8px" }}>
<a
href="#"
onClick={(e) => {
e.preventDefault()
vscode.postMessage(configMessage("global", language.t))
}}
<div>
<Button
type="button"
variant="secondary"
size="small"
icon="edit"
onClick={() => vscode.postMessage(configMessage("global", language.t))}
>
{language.t("provider.custom.edit.advanced")}
</a>
</Button>
</div>
</Show>
</div>
@@ -651,19 +665,49 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => {
{/* Models */}
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
<label
style={{
"font-size": "var(--kilo-font-size-12)",
"font-weight": "500",
color: "var(--text-weak-base)",
}}
>
{language.t("provider.custom.models.label")}
</label>
<Show when={fetching()}>
<Spinner style={{ width: "12px", height: "12px" }} />
</Show>
<div
style={{
display: "flex",
"justify-content": "space-between",
"align-items": "center",
"flex-wrap": "wrap",
gap: "8px",
}}
>
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
<label
style={{
"font-size": "var(--kilo-font-size-12)",
"font-weight": "500",
color: "var(--text-weak-base)",
}}
>
{language.t("provider.custom.models.label")}
</label>
<Show when={fetching()}>
<Spinner style={{ width: "12px", height: "12px" }} />
</Show>
</div>
<div style={{ display: "flex", gap: "8px", "align-items": "center", "flex-wrap": "wrap" }}>
<Button
type="button"
size="small"
variant="ghost"
onClick={toggleAllReasoning}
disabled={form.models.length === 0}
>
{language.t("provider.custom.models.toggleReasoning")}
</Button>
<Button
type="button"
size="small"
variant="ghost"
onClick={toggleAllImages}
disabled={form.models.length === 0}
>
{language.t("provider.custom.models.toggleImages")}
</Button>
</div>
</div>
<For each={form.models}>
{(m, i) => (
@@ -98,42 +98,44 @@ export function ModelCard(props: ModelCardProps) {
/>
</div>
{/* Reasoning toggle */}
<label
style={{
display: "flex",
"align-items": "center",
gap: "8px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-13)",
color: "var(--vscode-foreground)",
}}
>
<input
type="checkbox"
checked={props.m.reasoning}
onChange={(e) => props.onChangeReasoning(e.currentTarget.checked)}
/>
{props.t("provider.custom.models.reasoning.label")}
</label>
{/* Reasoning and Image toggles */}
<div style={{ display: "flex", gap: "16px", "align-items": "center", "flex-wrap": "wrap" }}>
<label
style={{
display: "flex",
"align-items": "center",
gap: "8px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-13)",
color: "var(--vscode-foreground)",
}}
>
<input
type="checkbox"
checked={props.m.reasoning}
onChange={(e) => props.onChangeReasoning(e.currentTarget.checked)}
/>
{props.t("provider.custom.models.reasoning.label")}
</label>
<label
style={{
display: "flex",
"align-items": "center",
gap: "8px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-13)",
color: "var(--vscode-foreground)",
}}
>
<input
type="checkbox"
checked={props.m.supportsImages}
onChange={(e) => props.onChangeSupportsImages(e.currentTarget.checked)}
/>
{props.t("provider.custom.models.modalities.image")}
</label>
<label
style={{
display: "flex",
"align-items": "center",
gap: "8px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-13)",
color: "var(--vscode-foreground)",
}}
>
<input
type="checkbox"
checked={props.m.supportsImages}
onChange={(e) => props.onChangeSupportsImages(e.currentTarget.checked)}
/>
{props.t("provider.custom.models.modalities.image")}
</label>
</div>
<Show when={issue()}>
{(error) => (
+2
View File
@@ -443,6 +443,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "الاسم المعروض",
"provider.custom.models.reasoning.label": "الاستدلال",
"provider.custom.models.modalities.image": "صورة",
"provider.custom.models.toggleReasoning": "تبديل التفكير للكل",
"provider.custom.models.toggleImages": "تبديل الصور للكل",
"provider.custom.models.remove": "إزالة النموذج",
"provider.custom.models.add": "إضافة نموذج",
"provider.custom.models.fetch.authError": "فشلت المصادقة. تحقق من مفتاح API أعلاه وحاول مرة أخرى.",
+2
View File
@@ -454,6 +454,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Nome de Exibição",
"provider.custom.models.reasoning.label": "Raciocínio",
"provider.custom.models.modalities.image": "Imagem",
"provider.custom.models.toggleReasoning": "Alternar raciocínio para todos",
"provider.custom.models.toggleImages": "Alternar imagem para todos",
"provider.custom.models.remove": "Remover modelo",
"provider.custom.models.add": "Adicionar modelo",
"provider.custom.models.fetch.authError": "Falha na autenticação. Verifique a chave de API acima e tente novamente.",
+2
View File
@@ -496,6 +496,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Naziv za prikaz",
"provider.custom.models.reasoning.label": "Zaključivanje",
"provider.custom.models.modalities.image": "Slika",
"provider.custom.models.toggleReasoning": "Uključi/isključi rezonovanje za sve",
"provider.custom.models.toggleImages": "Uključi/isključi slike za sve",
"provider.custom.models.remove": "Ukloni model",
"provider.custom.models.add": "Dodaj model",
"provider.custom.models.fetch.authError":
+2
View File
@@ -494,6 +494,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Visningsnavn",
"provider.custom.models.reasoning.label": "Ræsonnement",
"provider.custom.models.modalities.image": "Billede",
"provider.custom.models.toggleReasoning": "Slå ræsonnement til/fra for alle",
"provider.custom.models.toggleImages": "Slå billede til/fra for alle",
"provider.custom.models.remove": "Fjern model",
"provider.custom.models.add": "Tilføj model",
"provider.custom.models.fetch.authError": "Godkendelse mislykkedes. Kontrollér API-nøglen ovenfor, og prøv igen.",
@@ -504,6 +504,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Anzeigename",
"provider.custom.models.reasoning.label": "Schlussfolgerung",
"provider.custom.models.modalities.image": "Bild",
"provider.custom.models.toggleReasoning": "Schlussfolgerung für alle umschalten",
"provider.custom.models.toggleImages": "Bild für alle umschalten",
"provider.custom.models.remove": "Modell entfernen",
"provider.custom.models.add": "Modell hinzufügen",
"provider.custom.models.fetch.authError":
@@ -408,6 +408,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Display Name",
"provider.custom.models.reasoning.label": "Reasoning",
"provider.custom.models.modalities.image": "Image",
"provider.custom.models.toggleReasoning": "Toggle reasoning for all",
"provider.custom.models.toggleImages": "Toggle image for all",
"provider.custom.models.remove": "Remove model",
"provider.custom.models.add": "Add model",
"provider.custom.models.fetch.authError": "Authentication failed. Check the API key above and try again.",
+2
View File
@@ -497,6 +497,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Nombre para mostrar",
"provider.custom.models.reasoning.label": "Razonamiento",
"provider.custom.models.modalities.image": "Imagen",
"provider.custom.models.toggleReasoning": "Alternar razonamiento para todos",
"provider.custom.models.toggleImages": "Alternar imagen para todos",
"provider.custom.models.remove": "Eliminar modelo",
"provider.custom.models.add": "Añadir modelo",
"provider.custom.models.fetch.authError":
+2
View File
@@ -409,6 +409,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "نام نمایشی",
"provider.custom.models.reasoning.label": "استدلال",
"provider.custom.models.modalities.image": "تصویر",
"provider.custom.models.toggleReasoning": "تغییر وضعیت استدلال برای همه",
"provider.custom.models.toggleImages": "تغییر وضعیت تصویر برای همه",
"provider.custom.models.remove": "حذف مدل",
"provider.custom.models.add": "افزودن مدل",
"provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.",
+2
View File
@@ -498,6 +498,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Nom d'affichage",
"provider.custom.models.reasoning.label": "Raisonnement",
"provider.custom.models.modalities.image": "Image",
"provider.custom.models.toggleReasoning": "Basculer le raisonnement pour tous",
"provider.custom.models.toggleImages": "Basculer l'image pour tous",
"provider.custom.models.remove": "Supprimer le modèle",
"provider.custom.models.add": "Ajouter un modèle",
"provider.custom.models.fetch.authError": "Échec de l'authentification. Vérifiez la clé API ci-dessus et réessayez.",
+2
View File
@@ -318,6 +318,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Nome visualizzato",
"provider.custom.models.reasoning.label": "Reasoning",
"provider.custom.models.modalities.image": "Immagine",
"provider.custom.models.toggleReasoning": "Attiva/disattiva reasoning per tutti",
"provider.custom.models.toggleImages": "Attiva/disattiva immagine per tutti",
"provider.custom.models.remove": "Rimuovi modello",
"provider.custom.models.add": "Aggiungi modello",
"provider.custom.models.fetch.authError": "Autenticazione non riuscita. Controlla l'API key sopra e riprova.",
+2
View File
@@ -490,6 +490,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "表示名",
"provider.custom.models.reasoning.label": "推論",
"provider.custom.models.modalities.image": "画像",
"provider.custom.models.toggleReasoning": "すべてのモデルの推論を切り替え",
"provider.custom.models.toggleImages": "すべてのモデルの画像を切り替え",
"provider.custom.models.remove": "モデルを削除",
"provider.custom.models.add": "モデルを追加",
"provider.custom.models.fetch.authError": "認証に失敗しました。上記のAPIキーを確認して再試行してください。",
+2
View File
@@ -451,6 +451,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "표시 이름",
"provider.custom.models.reasoning.label": "추론",
"provider.custom.models.modalities.image": "이미지",
"provider.custom.models.toggleReasoning": "모든 모델의 추론 전환",
"provider.custom.models.toggleImages": "모든 모델의 이미지 전환",
"provider.custom.models.remove": "모델 제거",
"provider.custom.models.add": "모델 추가",
"provider.custom.models.fetch.authError": "인증에 실패했습니다. 위의 API 키를 확인하고 다시 시도하세요.",
+2
View File
@@ -447,6 +447,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Weergavenaam",
"provider.custom.models.reasoning.label": "Redeneren",
"provider.custom.models.modalities.image": "Afbeelding",
"provider.custom.models.toggleReasoning": "Redeneren voor alle modellen in-/uitschakelen",
"provider.custom.models.toggleImages": "Afbeelding voor alle modellen in-/uitschakelen",
"provider.custom.models.remove": "Model verwijderen",
"provider.custom.models.add": "Model toevoegen",
"provider.custom.models.fetch.authError":
+2
View File
@@ -457,6 +457,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Visningsnavn",
"provider.custom.models.reasoning.label": "Resonnering",
"provider.custom.models.modalities.image": "Bilde",
"provider.custom.models.toggleReasoning": "Veksle resonnering for alle",
"provider.custom.models.toggleImages": "Veksle bilde for alle",
"provider.custom.models.remove": "Fjern modell",
"provider.custom.models.add": "Legg til modell",
"provider.custom.models.fetch.authError": "Autentisering mislyktes. Sjekk API-nøkkelen ovenfor og prøv igjen.",
+2
View File
@@ -452,6 +452,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Nazwa wyświetlana",
"provider.custom.models.reasoning.label": "Rozumowanie",
"provider.custom.models.modalities.image": "Obraz",
"provider.custom.models.toggleReasoning": "Przełącz wnioskowanie dla wszystkich",
"provider.custom.models.toggleImages": "Przełącz obraz dla wszystkich",
"provider.custom.models.remove": "Usuń model",
"provider.custom.models.add": "Dodaj model",
"provider.custom.models.fetch.authError":
+2
View File
@@ -491,6 +491,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Отображаемое имя",
"provider.custom.models.reasoning.label": "Рассуждение",
"provider.custom.models.modalities.image": "Изображение",
"provider.custom.models.toggleReasoning": "Переключить рассуждения для всех",
"provider.custom.models.toggleImages": "Переключить изображения для всех",
"provider.custom.models.remove": "Удалить модель",
"provider.custom.models.add": "Добавить модель",
"provider.custom.models.fetch.authError": "Ошибка аутентификации. Проверьте API-ключ выше и попробуйте снова.",
+2
View File
@@ -488,6 +488,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "ชื่อที่แสดง",
"provider.custom.models.reasoning.label": "การใช้เหตุผล",
"provider.custom.models.modalities.image": "รูปภาพ",
"provider.custom.models.toggleReasoning": "สลับการให้เหตุผลสำหรับทุกโมเดล",
"provider.custom.models.toggleImages": "สลับรูปภาพสำหรับทุกโมเดล",
"provider.custom.models.remove": "ลบโมเดล",
"provider.custom.models.add": "เพิ่มโมเดล",
"provider.custom.models.fetch.authError": "การยืนยันตัวตนล้มเหลว ตรวจสอบคีย์ API ด้านบนแล้วลองอีกครั้ง",
+2
View File
@@ -442,6 +442,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Görünen Ad",
"provider.custom.models.reasoning.label": "Akıl Yürütme",
"provider.custom.models.modalities.image": "Görüntü",
"provider.custom.models.toggleReasoning": "Tüm modeller için akıl yürütmeyi aç/kapat",
"provider.custom.models.toggleImages": "Tüm modeller için görüntüyü aç/kapat",
"provider.custom.models.remove": "Modeli kaldır",
"provider.custom.models.add": "Model ekle",
"provider.custom.models.fetch.authError":
+2
View File
@@ -446,6 +446,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "Відображувана назва",
"provider.custom.models.reasoning.label": "Міркування",
"provider.custom.models.modalities.image": "Зображення",
"provider.custom.models.toggleReasoning": "Перемкнути міркування для всіх",
"provider.custom.models.toggleImages": "Перемкнути зображення для всіх",
"provider.custom.models.remove": "Видалити модель",
"provider.custom.models.add": "Додати модель",
"provider.custom.models.fetch.authError": "Автентифікація не вдалася. Перевірте API-ключ вище і спробуйте ще раз.",
+2
View File
@@ -473,6 +473,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "显示名称",
"provider.custom.models.reasoning.label": "推理",
"provider.custom.models.modalities.image": "图片",
"provider.custom.models.toggleReasoning": "切换所有模型的推理",
"provider.custom.models.toggleImages": "切换所有模型的图像",
"provider.custom.models.remove": "移除模型",
"provider.custom.models.add": "添加模型",
"provider.custom.models.fetch.authError": "认证失败。请检查上方的 API 密钥后重试。",
+2
View File
@@ -433,6 +433,8 @@ export const dict = {
"provider.custom.models.name.placeholder": "顯示名稱",
"provider.custom.models.reasoning.label": "推理",
"provider.custom.models.modalities.image": "圖片",
"provider.custom.models.toggleReasoning": "切換所有模型的推理",
"provider.custom.models.toggleImages": "切換所有模型的圖像",
"provider.custom.models.remove": "移除模型",
"provider.custom.models.add": "新增模型",
"provider.custom.models.fetch.authError": "驗證失敗。請檢查上方的 API 金鑰後重試。",