|
108 | 108 | // download lines wouldn't disappear after the 3s auto-dismiss timer. |
109 | 109 | let downloads = new SvelteMap<string, DownloadProgress>(); |
110 | 110 | let statusMessage = $state<string | null>(null); |
| 111 | + // Optional inline action attached to the current statusMessage. Used by |
| 112 | + // `savedToast()` to surface a "Restart now" button on the same toast when |
| 113 | + // a config save would otherwise require manual Stop → Start. `null` = |
| 114 | + // plain informational toast (legacy behavior). |
| 115 | + let statusAction = $state<{ label: string; onClick: () => Promise<void> } | null>(null); |
111 | 116 | let typedEnvKeys = $state<Set<string>>(new Set()); |
112 | 117 | let catalog = $state<Catalog>({ families: [], recommended: [], embeddings: [] }); |
113 | 118 | let systemInfo = $state<SystemInfo | null>(null); |
|
435 | 440 | config = await api.updateServerConfig(config.server); |
436 | 441 | } |
437 | 442 |
|
438 | | - // Shared toast text — restart hint only when the server is currently |
439 | | - // running (otherwise just "Saved", since next start will pick the new |
440 | | - // env vars naturally). Without this, users who change TurboQuant / |
441 | | - // KV-quant / ctx caps while the server is running see the UI update |
442 | | - // but the running process keeps the OLD env vars until manual |
443 | | - // Stop → Start. The QUANT / CONTEXT / SERVER cards previously had no |
444 | | - // hint at all (only env_overrides did). |
445 | | - function savedToast(): string { |
| 443 | + // One-click "Stop → wait → Start" — used by the inline action button on |
| 444 | + // the savedToast when the server is running. Polls `serverStatus` until |
| 445 | + // it drains to `stopped` (or `errored`) before issuing `startServer` so |
| 446 | + // the new env vars from `spawn_server_command` are actually used. Without |
| 447 | + // the drain wait, `startServer` would race the still-tearing-down |
| 448 | + // supervisor and surface a stale state. |
| 449 | + async function restartServer() { |
| 450 | + statusMessage = t("config.restarting"); |
| 451 | + statusAction = null; |
| 452 | + try { |
| 453 | + if (status.state === "running" || status.state === "starting") { |
| 454 | + status = await api.stopServer(); |
| 455 | + let waited = 0; |
| 456 | + const POLL_MS = 200; |
| 457 | + const TIMEOUT_MS = 30_000; |
| 458 | + while ( |
| 459 | + status.state !== "stopped" && |
| 460 | + status.state !== "errored" && |
| 461 | + waited < TIMEOUT_MS |
| 462 | + ) { |
| 463 | + await new Promise((r) => setTimeout(r, POLL_MS)); |
| 464 | + status = await api.serverStatus(); |
| 465 | + waited += POLL_MS; |
| 466 | + } |
| 467 | + } |
| 468 | + status = await api.startServer(); |
| 469 | + statusMessage = t("config.restarted"); |
| 470 | + setTimeout(() => (statusMessage = null), 2000); |
| 471 | + } catch (e) { |
| 472 | + statusMessage = String(e); |
| 473 | + setTimeout(() => (statusMessage = null), 4000); |
| 474 | + } |
| 475 | + } |
| 476 | +
|
| 477 | + // Shared post-save toast. Two modes: |
| 478 | + // * server running → "Saved. Restart to apply" + inline [Restart now] |
| 479 | + // button (calls `restartServer`). |
| 480 | + // * server stopped → plain "Saved." (next start picks up the change |
| 481 | + // naturally — no restart needed). |
| 482 | + // Without this, users who change TurboQuant / KV-quant / ctx caps while |
| 483 | + // the server is running see the UI update but the running process keeps |
| 484 | + // the OLD env vars until manual Stop → Start. The QUANT / CONTEXT / |
| 485 | + // SERVER cards previously had no hint at all (only env_overrides did). |
| 486 | + function savedToast() { |
446 | 487 | if (status.state === "running" || status.state === "starting") { |
447 | | - return t("config.savedRestartHint"); |
| 488 | + statusMessage = t("config.savedRestartHint"); |
| 489 | + statusAction = { label: t("config.restartNow"), onClick: restartServer }; |
| 490 | + setTimeout(() => { |
| 491 | + if (statusMessage === t("config.savedRestartHint")) { |
| 492 | + statusMessage = null; |
| 493 | + statusAction = null; |
| 494 | + } |
| 495 | + }, 6000); |
| 496 | + } else { |
| 497 | + statusMessage = t("config.saved"); |
| 498 | + statusAction = null; |
| 499 | + setTimeout(() => { |
| 500 | + if (statusMessage === t("config.saved")) { |
| 501 | + statusMessage = null; |
| 502 | + } |
| 503 | + }, 2000); |
448 | 504 | } |
449 | | - return t("config.saved"); |
450 | 505 | } |
451 | 506 |
|
452 | 507 | async function saveServer() { |
453 | 508 | if (!config) return; |
454 | 509 | config = await api.updateServerConfig(config.server); |
455 | | - statusMessage = savedToast(); |
456 | | - setTimeout(() => (statusMessage = null), 3000); |
| 510 | + savedToast(); |
457 | 511 | } |
458 | 512 |
|
459 | 513 | async function saveQuant() { |
460 | 514 | if (!config) return; |
461 | 515 | config = await api.updateQuantConfig(config.quant); |
462 | | - statusMessage = savedToast(); |
463 | | - setTimeout(() => (statusMessage = null), 3000); |
| 516 | + savedToast(); |
464 | 517 | } |
465 | 518 |
|
466 | 519 | async function saveContext() { |
|
469 | 522 | // ctx affects KV-cache headroom in the tuned memory recommendation |
470 | 523 | // (~1 GB per 8K tokens). Re-sync so saved caps follow. |
471 | 524 | await syncTunedMemoryCaps(); |
472 | | - statusMessage = savedToast(); |
473 | | - setTimeout(() => (statusMessage = null), 3000); |
| 525 | + savedToast(); |
474 | 526 | } |
475 | 527 |
|
476 | 528 | async function resetMemoryCaps() { |
|
635 | 687 | {/if} |
636 | 688 | </div> |
637 | 689 | <div class="ml-auto flex items-center gap-2.5"> |
638 | | - {#if statusMessage}<span class="dim">{statusMessage}</span>{/if} |
| 690 | + {#if statusMessage} |
| 691 | + <span class="dim">{statusMessage}</span> |
| 692 | + {#if statusAction} |
| 693 | + <button |
| 694 | + class="text-[11px] px-2 py-0.5 border border-accent text-accent rounded-md hover:bg-accent/6 transition-colors" |
| 695 | + onclick={statusAction.onClick} |
| 696 | + >{statusAction.label}</button> |
| 697 | + {/if} |
| 698 | + {/if} |
639 | 699 | {#if memoryUsage} |
640 | 700 | {@const usedGb = memoryUsage.used_bytes / 1024 ** 3} |
641 | 701 | {@const totalGb = memoryUsage.total_bytes / 1024 ** 3} |
|
0 commit comments