mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
Merge pull request #7545 from Kilo-Org/kirillk/picker-adjustments
fix(vscode): UX improvements for pickers, settings, and icons
This commit is contained in:
@@ -60,6 +60,7 @@ const ModelsTab: Component = () => {
|
||||
placement="bottom-start"
|
||||
allowClear
|
||||
clearLabel={language.t("settings.providers.notSet")}
|
||||
includeAutoSmall
|
||||
/>
|
||||
</SettingsRow>
|
||||
</Card>
|
||||
|
||||
@@ -227,29 +227,20 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
|
||||
{/* Save bar — visible when there are unsaved config changes */}
|
||||
<Show when={isDirty()}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "flex-end",
|
||||
gap: "8px",
|
||||
padding: "8px 16px",
|
||||
"border-top": "1px solid var(--border-weak-base)",
|
||||
}}
|
||||
>
|
||||
<span style={{ "font-size": "12px", color: "var(--foreground-secondary)", "margin-right": "auto" }}>
|
||||
{language.t("settings.saveBar.unsavedChanges")}
|
||||
</span>
|
||||
<Button variant="ghost" size="small" onClick={discardConfig}>
|
||||
{language.t("settings.saveBar.discard")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={handleSave}>
|
||||
{language.t("settings.saveBar.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
{/* Save bar — slides in when there are unsaved config changes */}
|
||||
<div
|
||||
class={`settings-save-bar${isDirty() ? " settings-save-bar--visible" : ""}`}
|
||||
inert={!isDirty() || undefined}
|
||||
aria-hidden={!isDirty()}
|
||||
>
|
||||
<span class="settings-save-bar-label">{language.t("settings.saveBar.unsavedChanges")}</span>
|
||||
<Button variant="ghost" size="small" onClick={discardConfig}>
|
||||
{language.t("settings.saveBar.discard")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={handleSave}>
|
||||
{language.t("settings.saveBar.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ export const ModeSwitcherBase: Component<ModeSwitcherBaseProps> = (props) => {
|
||||
<PopupSelector
|
||||
expanded={false}
|
||||
placement="top-start"
|
||||
preferredHeight={300}
|
||||
minHeight={100}
|
||||
open={open()}
|
||||
onOpenChange={onOpen}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
||||
const [preActiveIdx, setPreActiveIdx] = createSignal(-1)
|
||||
const [previewIdx, setPreviewIdx] = createSignal(-1)
|
||||
const [previewHeight, setPreviewHeight] = createSignal(320)
|
||||
const [previewHeight, setPreviewHeight] = createSignal(500)
|
||||
|
||||
let searchRef: HTMLInputElement | undefined
|
||||
let searchWrapperRef: HTMLDivElement | undefined
|
||||
@@ -324,152 +324,161 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
}
|
||||
class={`model-selector-popover${expanded() ? " model-selector-popover--expanded" : ""}`}
|
||||
>
|
||||
{(bodyH) => (
|
||||
<div
|
||||
onKeyDown={handleKeyDown}
|
||||
class={`model-selector-body${expanded() ? " model-selector-body--expanded" : ""}`}
|
||||
style={{ height: `${bodyH()}px` }}
|
||||
ref={bodyRef}
|
||||
>
|
||||
<div class="model-selector-search-wrapper" ref={searchWrapperRef}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
class="model-selector-search"
|
||||
type="text"
|
||||
placeholder={language.t("dialog.model.search.placeholder")}
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<Tooltip
|
||||
value={expanded() ? language.t("dialog.model.collapse") : language.t("dialog.model.expand")}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
icon={expanded() ? "collapse" : "expand"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setExpanded((v) => {
|
||||
if (v) {
|
||||
setPreActiveIdx(-1)
|
||||
setPreviewIdx(-1)
|
||||
}
|
||||
return !v
|
||||
})
|
||||
requestAnimationFrame(() => {
|
||||
searchRef?.focus()
|
||||
listRef?.querySelector(".model-selector-item.active")?.scrollIntoView({ block: "nearest" })
|
||||
})
|
||||
}}
|
||||
{(bodyH) => {
|
||||
createEffect(() => {
|
||||
if (!expanded()) return
|
||||
const h = bodyH()
|
||||
if (h === undefined) return
|
||||
const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0)
|
||||
setPreviewHeight((h - chrome) / 2)
|
||||
})
|
||||
return (
|
||||
<div
|
||||
onKeyDown={handleKeyDown}
|
||||
class={`model-selector-body${expanded() ? " model-selector-body--expanded" : ""}`}
|
||||
style={{ height: `${bodyH()}px` }}
|
||||
ref={bodyRef}
|
||||
>
|
||||
<div class="model-selector-search-wrapper" ref={searchWrapperRef}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
class="model-selector-search"
|
||||
type="text"
|
||||
placeholder={language.t("dialog.model.search.placeholder")}
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="model-selector-list" role="listbox" ref={listRef}>
|
||||
<Show when={flatFiltered().length === 0 && !props.allowClear}>
|
||||
<div class="model-selector-empty">{language.t("dialog.model.empty")}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.allowClear}>
|
||||
<div
|
||||
class={`model-selector-item${isSelected(0) && !pointer() ? " keyboard-focused" : ""}${isSelected(0) ? " selected" : ""}${!props.value?.providerID ? " active" : ""}`}
|
||||
role="option"
|
||||
aria-selected={!props.value?.providerID}
|
||||
onClick={() => pickClear()}
|
||||
onMouseMove={() => {
|
||||
setPointer(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (pointer()) setSelectedIndex(0)
|
||||
}}
|
||||
<Tooltip
|
||||
value={expanded() ? language.t("dialog.model.collapse") : language.t("dialog.model.expand")}
|
||||
placement="top"
|
||||
>
|
||||
<span class="model-selector-item-name" style={{ "font-style": "italic", opacity: 0.7 }}>
|
||||
{props.clearLabel ?? language.t("dialog.model.notSet")}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<IconButton
|
||||
icon={expanded() ? "collapse" : "expand"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setExpanded((v) => {
|
||||
if (v) {
|
||||
setPreActiveIdx(-1)
|
||||
setPreviewIdx(-1)
|
||||
}
|
||||
return !v
|
||||
})
|
||||
requestAnimationFrame(() => {
|
||||
searchRef?.focus()
|
||||
listRef?.querySelector(".model-selector-item.active")?.scrollIntoView({ block: "nearest" })
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
<>
|
||||
<div class="model-selector-group-label">{group.providerName}</div>
|
||||
<For each={group.models}>
|
||||
{(model) => {
|
||||
const idx = () => flatIndexMap().get(model) ?? 0
|
||||
const hovered = () => isSelected(idx())
|
||||
const preActive = () => isPreActive(idx())
|
||||
const showSelectBtn = () => expanded() && preActive() && !isActive(model)
|
||||
return (
|
||||
<div
|
||||
class={`model-selector-item${(hovered() && !pointer()) || preActive() ? " keyboard-focused" : ""}${hovered() || preActive() ? " selected" : ""}${isActive(model) ? " active" : ""}`}
|
||||
role="option"
|
||||
aria-selected={isActive(model)}
|
||||
onClick={() => {
|
||||
setSelectedIndex(idx())
|
||||
setPreActiveIdx(idx())
|
||||
setPreviewIdx(idx())
|
||||
if (!expanded()) pick(model)
|
||||
searchRef?.focus()
|
||||
}}
|
||||
onDblClick={() => {
|
||||
if (expanded()) pick(model)
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
setPointer(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (pointer()) setSelectedIndex(idx())
|
||||
}}
|
||||
>
|
||||
<div class="model-selector-item-left">
|
||||
<span class="model-selector-item-name">
|
||||
{(() => {
|
||||
const full = sanitizeName(model.name)
|
||||
const sep = full.indexOf(": ")
|
||||
if (sep < 0) return <span class="model-selector-item-name-main">{full}</span>
|
||||
return (
|
||||
<>
|
||||
<span class="model-selector-item-name-provider">{full.slice(0, sep)}</span>
|
||||
<span class="model-selector-item-name-main">{full.slice(sep + 2)}</span>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<Tag data-variant="member">{language.t("model.tag.free")}</Tag>
|
||||
<div class="model-selector-list" role="listbox" ref={listRef}>
|
||||
<Show when={flatFiltered().length === 0 && !props.allowClear}>
|
||||
<div class="model-selector-empty">{language.t("dialog.model.empty")}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.allowClear}>
|
||||
<div
|
||||
class={`model-selector-item${isSelected(0) && !pointer() ? " keyboard-focused" : ""}${isSelected(0) ? " selected" : ""}${!props.value?.providerID ? " active" : ""}`}
|
||||
role="option"
|
||||
aria-selected={!props.value?.providerID}
|
||||
onClick={() => pickClear()}
|
||||
onMouseMove={() => {
|
||||
setPointer(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (pointer()) setSelectedIndex(0)
|
||||
}}
|
||||
>
|
||||
<span class="model-selector-item-name" style={{ "font-style": "italic", opacity: 0.7 }}>
|
||||
{props.clearLabel ?? language.t("dialog.model.notSet")}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
<>
|
||||
<div class="model-selector-group-label">{group.providerName}</div>
|
||||
<For each={group.models}>
|
||||
{(model) => {
|
||||
const idx = () => flatIndexMap().get(model) ?? 0
|
||||
const hovered = () => isSelected(idx())
|
||||
const preActive = () => isPreActive(idx())
|
||||
const showSelectBtn = () => expanded() && preActive() && !isActive(model)
|
||||
return (
|
||||
<div
|
||||
class={`model-selector-item${(hovered() && !pointer()) || preActive() ? " keyboard-focused" : ""}${hovered() || preActive() ? " selected" : ""}${isActive(model) ? " active" : ""}`}
|
||||
role="option"
|
||||
aria-selected={isActive(model)}
|
||||
onClick={() => {
|
||||
setSelectedIndex(idx())
|
||||
setPreActiveIdx(idx())
|
||||
setPreviewIdx(idx())
|
||||
if (!expanded()) pick(model)
|
||||
searchRef?.focus()
|
||||
}}
|
||||
onDblClick={() => {
|
||||
if (expanded()) pick(model)
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
setPointer(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (pointer()) setSelectedIndex(idx())
|
||||
}}
|
||||
>
|
||||
<div class="model-selector-item-left">
|
||||
<span class="model-selector-item-name">
|
||||
{(() => {
|
||||
const full = sanitizeName(model.name)
|
||||
const sep = full.indexOf(": ")
|
||||
if (sep < 0) return <span class="model-selector-item-name-main">{full}</span>
|
||||
return (
|
||||
<>
|
||||
<span class="model-selector-item-name-provider">{full.slice(0, sep)}</span>
|
||||
<span class="model-selector-item-name-main">{full.slice(sep + 2)}</span>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<Tag data-variant="member">{language.t("model.tag.free")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<button
|
||||
class={`model-selector-item-select-btn${showSelectBtn() ? "" : " model-selector-item-select-btn--hidden"}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
pick(model)
|
||||
}}
|
||||
>
|
||||
{language.t("dialog.model.select")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<button
|
||||
class={`model-selector-item-select-btn${showSelectBtn() ? "" : " model-selector-item-select-btn--hidden"}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
pick(model)
|
||||
}}
|
||||
>
|
||||
{language.t("dialog.model.select")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={expanded()}>
|
||||
<div class="model-selector-splitter" ref={splitterRef} onMouseDown={onSplitterMouseDown} />
|
||||
</Show>
|
||||
<div
|
||||
class={`model-selector-preview${expanded() ? " model-selector-preview--visible" : ""}`}
|
||||
style={expanded() ? { height: `${previewHeight()}px` } : {}}
|
||||
>
|
||||
<ModelPreview model={previewModel() ?? activeModel() ?? null} />
|
||||
<Show when={expanded()}>
|
||||
<div class="model-selector-splitter" ref={splitterRef} onMouseDown={onSplitterMouseDown} />
|
||||
</Show>
|
||||
<div
|
||||
class={`model-selector-preview${expanded() ? " model-selector-preview--visible" : ""}`}
|
||||
style={expanded() ? { height: `${previewHeight()}px` } : {}}
|
||||
>
|
||||
<ModelPreview model={previewModel() ?? activeModel() ?? null} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
</PopupSelector>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ export function PopupSelector<T extends ValidComponent = ValidComponent>(props:
|
||||
|
||||
const bodyH = createMemo(() => {
|
||||
const preferred = local.expanded ? local.preferredExpandedHeight : local.preferredHeight
|
||||
if (preferred === undefined) return undefined
|
||||
const h = panelH()
|
||||
if (h === undefined) return preferred
|
||||
// 26px = 2px border + 24px popover-body padding (12px top + 12px bottom)
|
||||
const max = h - 26
|
||||
const max = h !== undefined ? h - 26 : undefined
|
||||
if (preferred === undefined) return max !== undefined ? Math.max(local.minHeight ?? 100, max) : undefined
|
||||
if (max === undefined) return preferred
|
||||
return Math.max(local.minHeight ?? 100, Math.min(preferred, max))
|
||||
})
|
||||
|
||||
|
||||
@@ -277,10 +277,18 @@
|
||||
border-color: var(--vscode-textLink-foreground, #3794ff);
|
||||
}
|
||||
|
||||
.feedback-button [data-component="icon"] {
|
||||
--icon-base: currentColor;
|
||||
}
|
||||
|
||||
.feedback-button:hover [data-slot="icon-svg"] {
|
||||
color: var(--vscode-editor-background, #1e1e1e);
|
||||
}
|
||||
|
||||
.feedback-button:hover [data-component="icon"] {
|
||||
--icon-base: var(--vscode-editor-background, #1e1e1e);
|
||||
}
|
||||
|
||||
/* Feedback Dialog */
|
||||
|
||||
@keyframes feedback-dialog-enter {
|
||||
@@ -442,6 +450,10 @@
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.scroll-to-bottom-button [data-component="icon"] {
|
||||
--icon-base: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.scroll-to-bottom-button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
@@ -2797,6 +2809,35 @@ body.vscode-light
|
||||
animation: enhance-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Settings save bar */
|
||||
.settings-save-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
border-top: 1px solid var(--border-weak-base);
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-height 0.2s ease,
|
||||
opacity 0.2s ease,
|
||||
padding 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-save-bar--visible {
|
||||
max-height: 52px;
|
||||
opacity: 1;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.settings-save-bar-label {
|
||||
font-size: 12px;
|
||||
color: var(--foreground-secondary);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
[data-slot="settings-row"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user