From fdb39bdbd683484d35f0874f83c6dd1957e021aa Mon Sep 17 00:00:00 2001 From: Kyle Poineal Date: Sun, 12 Jul 2026 10:58:54 -0600 Subject: [PATCH] feat(gui3): add loaded model count and unload button to chat model picker Surfaces loaded model visibility and unload controls directly in the chat page model picker dropdown, addressing #2573. - Show loaded model count badge on the picker button - Add per-row unload button for loaded models in the dropdown - Unload button always visible (touch-friendly, no hover required) - aria-live announcement on unload completion - Loading/spinner state during unload operation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ui-redesign/src/components/ChatView.tsx | 62 +++++++++++++----- prototype/ui-redesign/src/styles/styles.css | 63 +++++++++++++++++++ 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/prototype/ui-redesign/src/components/ChatView.tsx b/prototype/ui-redesign/src/components/ChatView.tsx index ace112dd53..55afeac2c3 100644 --- a/prototype/ui-redesign/src/components/ChatView.tsx +++ b/prototype/ui-redesign/src/components/ChatView.tsx @@ -728,6 +728,8 @@ const ChatView: React.FC = ({ currentModel, loadedModels, onModel const [modelPickerQuery, setModelPickerQuery] = useState(''); const [modelPickerLoading, setModelPickerLoading] = useState(null); const [modelPickerError, setModelPickerError] = useState(null); + const [modelPickerUnloading, setModelPickerUnloading] = useState(null); + const [unloadAnnouncement, setUnloadAnnouncement] = useState(''); const [presetPickerOpen, setPresetPickerOpen] = useState(false); const [presetPickerQuery, setPresetPickerQuery] = useState(''); const [presetPickerApplying, setPresetPickerApplying] = useState(null); @@ -2369,6 +2371,19 @@ ${finalText}` setPersistHistory(prev => !prev); }, []); + const handleModelPickerUnload = useCallback(async (modelName: string, e: React.MouseEvent) => { + e.stopPropagation(); + if (modelPickerUnloading) return; + setModelPickerUnloading(modelName); + try { + await api.unloadModel(modelName); + await Promise.resolve(onRefresh()); + setUnloadAnnouncement(`${modelName} unloaded`); + } finally { + setModelPickerUnloading(null); + } + }, [modelPickerUnloading, onRefresh]); + const handleModelPickerSelect = useCallback(async (option: ModelPickerOption) => { if (option.loaded) { onModelSelect(option.name); @@ -2745,6 +2760,7 @@ ${finalText}` )} {/* Composer */} +
{unloadAnnouncement}
{(modelPickerOptions.length > 0 || modelPickerOpen) && ( @@ -2759,6 +2775,9 @@ ${finalText}` > {currentModel || 'Choose model'} + {selectableModels.length > 0 && ( + ({selectableModels.length}) + )} ▾ {modelPickerOpen && ( @@ -2774,22 +2793,37 @@ ${finalText}`
{modelPickerOptions.map(option => ( - + + {option.loaded && ( + + )} +
))} {modelPickerOptions.length === 0 &&
No matching models
}
diff --git a/prototype/ui-redesign/src/styles/styles.css b/prototype/ui-redesign/src/styles/styles.css index 512fee3ed5..b4104043f8 100644 --- a/prototype/ui-redesign/src/styles/styles.css +++ b/prototype/ui-redesign/src/styles/styles.css @@ -6489,6 +6489,69 @@ input, textarea { margin-left: auto; } +.composer__model-button-badge { + font-size: var(--text-xs); + color: var(--text-tertiary); + font-weight: 400; + flex-shrink: 0; +} + +.composer__model-option-row { + display: flex; + align-items: center; + gap: var(--space-1); + border-radius: var(--radius-md); +} + +.composer__model-option-row:hover, +.composer__model-option-row.is-active { + background: var(--accent-soft); + color: var(--text-primary); +} + +.composer__model-option-row .composer__model-option { + flex: 1; + min-width: 0; + border-radius: 0; +} + +.composer__model-option-row .composer__model-option:hover, +.composer__model-option-row .composer__model-option.is-active { + background: transparent; +} + +.composer__model-option-row.is-unloading .composer__model-option { + opacity: 0.6; + cursor: wait; +} + +.composer__model-option-unload { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + font-size: 14px; + line-height: 1; + color: var(--text-tertiary); + opacity: 1; + transition: color 0.12s, background 0.12s; + margin-right: var(--space-1); +} + +.composer__model-option-unload:hover { + color: var(--danger); + background: var(--danger-soft); +} + +.composer__model-option-row.is-unloading .composer__model-option-unload { + opacity: 1; + color: var(--text-tertiary); + cursor: wait; +} + .composer__model-error { margin-top: var(--space-2); padding: var(--space-2);