Files
windmill/frontend/src/lib/components/instanceSettings/SettingCard.svelte
T
GuilhemandClaude Fable 5 2e14302e4a feat(frontend): custom skills — detail modal, batch manage, shared validation (#9847)
* feat(frontend): simplify custom skills workspace settings UI

Collapse the "Custom skills" AI settings section into a single block. When
no skills exist, show two side-by-side zones: a drag-and-drop folder dropzone
(reusing FileInput) and a paste textarea whose add button appears only once
content is entered. When skills exist, an "+ Add skills" dropdown offers
"Import a folder of skills" (native picker) and "Paste a skill" (modal), above
the skills list. Folder ingestion is shared by both the picker and the dropzone
via processFolderFiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): custom skills detail modal + shared zod validation

Rework the Custom skills settings: header Add-skills dropdown, per-row
ellipsis menu (edit/delete), a Show more detail modal with a view/edit
toggle (rendered markdown in read mode), accent Save gated by dirty
detection and inline validation, and a folder-import conflict modal with
per-skill overwrite toggles. Extract skill parsing/validation into a
shared Zod-backed aiSkills module used by both the modal and the importer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(frontend): use Button for Show more; surface empty-body validation

Address review: swap the raw <button> Show-more affordance for the
design-system Button (per frontend component standards), and render the
Save/inline-error block whenever editing an existing skill so clearing
the body surfaces "body is required" instead of hiding both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(frontend): unit-test aiSkills; use themed border token

Add aiSkills.test.ts covering parseSkillMd (BOM, CRLF, malformed YAML),
validateSkill (code-point vs byte limits, name pattern), parseAndValidateSkill
(nameOverride precedence) and buildSkillMd round-trip. Replace the hardcoded
gray borders with the themed border-border-light token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(frontend): cap custom skills list height and scroll

Constrain the skills list to max-h-96 with overflow-y-auto so a large
number of skills scrolls within the section instead of pushing the page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(frontend): manage-mode batch delete for custom skills

Add a "Manage skills" button (shown only with more than one skill) that
enters a multi-select mode: a checkbox per row plus a sticky select-all
(tri-state) header, and a batch Delete gated on the selection with a
confirmation. Manage mode auto-exits when the list drops to one skill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(frontend): destructive delete, subtle manage button, Esc exits manage mode

Batch Delete uses the destructive accent variant, Manage skills uses the
subtle variant, and Escape leaves manage mode (mirroring Done) unless a
modal or menu is open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(frontend): add neutral 'info' type to ConfirmationModal; use for skill import

ConfirmationModal only had 'danger' and 'reload' semantics, so a
constructive confirmation like importing skills defaulted to danger
(red warning + destructive button). Add a neutral 'info' type (blue Info
icon, non-destructive accent confirm) and use it for the Import skills modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:43:50 +00:00

81 lines
1.8 KiB
Svelte

<script lang="ts">
import { twMerge } from 'tailwind-merge'
import { enterpriseLicense } from '$lib/stores'
import EEOnly from '../EEOnly.svelte'
import Tooltip from '../Tooltip.svelte'
import { Button } from '../common'
import type { ButtonType } from '../common/button/model'
interface Props {
label?: string
description?: string
ee_only?: string
tooltip?: string
settingKey?: string
actionButton?: {
label: string
onclick: (values: Record<string, any>) => Promise<void>
variant?: ButtonType.Variant
}
values?: Record<string, any>
children: import('svelte').Snippet
headerAction?: import('svelte').Snippet
class?: string
}
let {
label,
description,
ee_only,
tooltip,
settingKey,
actionButton,
values,
children,
headerAction,
class: clazz
}: Props = $props()
</script>
<div
data-setting-key={settingKey}
class={twMerge('p-4 rounded-md bg-surface-tertiary shadow-sm flex flex-col gap-1', clazz)}
>
{#if label}
<div class="flex items-center justify-between gap-2 w-full">
<div class="flex gap-1 items-baseline">
<span class="text-emphasis font-semibold text-xs">{label}</span>
{#if ee_only != undefined && !$enterpriseLicense}
{#if ee_only != ''}
<EEOnly>{ee_only}</EEOnly>
{:else}
<EEOnly />
{/if}
{/if}
{#if tooltip}
<Tooltip>{tooltip}</Tooltip>
{/if}
</div>
{#if headerAction}
{@render headerAction()}
{:else if actionButton}
<Button
disabled={ee_only != undefined && !$enterpriseLicense}
variant={actionButton.variant ?? 'default'}
unifiedSize="sm"
onclick={async () => await actionButton?.onclick(values ?? {})}
>
{actionButton.label}
</Button>
{/if}
</div>
{#if description}
<span class="text-secondary font-normal text-xs">
{@html description}
</span>
{/if}
{/if}
{@render children()}
</div>