studio: add cancel button for model loading/downloading

Adds a Cancel button next to the "Downloading model..." spinner so
users can abort long downloads. Clicking it aborts the in-flight load,
calls unloadModel to kill any running llama-server process, and clears
the loading state.
This commit is contained in:
Daniel Han 2026-03-15 06:24:32 +00:00
commit f5f631e5d1
2 changed files with 29 additions and 1 deletions

View file

@ -321,7 +321,7 @@ export function ChatPage(): ReactElement {
const modelsFromStore = useChatRuntimeStore((state) => state.models);
const lorasFromStore = useChatRuntimeStore((state) => state.loras);
const modelsError = useChatRuntimeStore((state) => state.modelsError);
const { refresh, selectModel, ejectModel, loadingModel } =
const { refresh, selectModel, ejectModel, cancelLoading, loadingModel } =
useChatModelRuntime();
const refreshRef = useRef(refresh);
const selectModelRef = useRef(selectModel);
@ -613,6 +613,13 @@ export function ChatPage(): ReactElement {
<span className="text-xs">
Downloading model
</span>
<button
type="button"
onClick={cancelLoading}
className="ml-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors"
>
Cancel
</button>
</div>
) : null}
</div>

View file

@ -147,6 +147,8 @@ export function useChatModelRuntime() {
id: string;
displayName: string;
} | null>(null);
const [loadAbortController, setLoadAbortController] =
useState<AbortController | null>(null);
const refresh = useCallback(async () => {
setModelsError(null);
@ -215,8 +217,11 @@ export function useChatModelRuntime() {
setModelsError(null);
setLoadingModel({ id: modelId, displayName });
const abortCtrl = new AbortController();
setLoadAbortController(abortCtrl);
try {
async function performLoad(): Promise<void> {
if (abortCtrl.signal.aborted) throw new Error("Cancelled");
let previousWasUnloaded = false;
const currentCheckpoint =
useChatRuntimeStore.getState().params.checkpoint;
@ -277,6 +282,7 @@ export function useChatModelRuntime() {
const loadPromise = performLoad().finally(() => {
setLoadingModel(null);
setLoadAbortController(null);
});
await toast.promise(loadPromise, {
@ -322,10 +328,25 @@ export function useChatModelRuntime() {
}
}, [clearCheckpoint, params.checkpoint, refresh, setModelsError]);
const cancelLoading = useCallback(async () => {
if (!loadingModel) return;
loadAbortController?.abort();
setLoadingModel(null);
setLoadAbortController(null);
try {
await unloadModel({ model_path: loadingModel.id });
} catch {
// Best-effort cleanup
}
clearCheckpoint();
toast.info("Model loading cancelled");
}, [loadingModel, loadAbortController, clearCheckpoint]);
return {
refresh,
selectModel,
ejectModel,
cancelLoading,
loadingModel,
};
}