Compare commits
3 commits
main
...
studio-lla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c7571299 | ||
|
|
6ffa4bfe90 | ||
|
|
66781abc58 |
7 changed files with 86 additions and 90 deletions
|
|
@ -30,6 +30,7 @@ class LlamaUpdateJob(BaseModel):
|
|||
message: str = ""
|
||||
from_tag: Optional[str] = None
|
||||
to_tag: Optional[str] = None
|
||||
reload_required: Optional[bool] = None
|
||||
error: Optional[str] = None
|
||||
progress: Optional[float] = Field(None, description = "0..1 while running, 1 on success.")
|
||||
started_at: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -84,12 +84,7 @@ def _write_install(
|
|||
asset: str | None = None,
|
||||
release_tag: str | None = None,
|
||||
) -> str:
|
||||
"""Create a fake prebuilt install tree and return the llama-server path.
|
||||
|
||||
``asset`` is the bundle filename recorded in the marker; omit it to model an
|
||||
older marker that predates asset-based ROCm forwarding (backward compat).
|
||||
``release_tag`` is the full release tag (e.g. a ``b9596-mix-<sha>`` mix
|
||||
build); defaults to ``tag`` for a plain prebuilt."""
|
||||
"""Create a fake prebuilt install and return the llama-server path."""
|
||||
bin_dir = dir_ / "build" / "bin"
|
||||
bin_dir.mkdir(parents = True, exist_ok = True)
|
||||
binary = bin_dir / "llama-server"
|
||||
|
|
@ -436,7 +431,6 @@ def test_start_update_happy_path(monkeypatch, tmp_path):
|
|||
assert res["job"]["from_tag"] == "b9493"
|
||||
assert res["job"]["progress"] == 0.0
|
||||
|
||||
# Wait for the background worker.
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
job = upd.get_update_status()["job"]
|
||||
|
|
@ -445,17 +439,44 @@ def test_start_update_happy_path(monkeypatch, tmp_path):
|
|||
time.sleep(0.05)
|
||||
assert job["state"] == "success", job
|
||||
assert job["to_tag"] == "b9518"
|
||||
# Installer was invoked with the resolved install dir + latest + repo.
|
||||
assert job["reload_required"] is False
|
||||
assert "--install-dir" in captured["cmd"]
|
||||
assert str(install_dir) in captured["cmd"]
|
||||
assert "--llama-tag" in captured["cmd"] and "latest" in captured["cmd"]
|
||||
assert "unslothai/llama.cpp" in captured["cmd"]
|
||||
# Progress lines were parsed and success pins progress at 1.0.
|
||||
assert job["progress"] == 1.0
|
||||
# The worker asks the installer for fine-grained progress milestones.
|
||||
assert popen_kwargs["env"]["UNSLOTH_PROGRESS_PERCENT_STEP"] == "5"
|
||||
|
||||
|
||||
def test_start_update_reports_full_release_tag(monkeypatch, tmp_path):
|
||||
install_dir = tmp_path / "llama.cpp"
|
||||
binary = _write_install(install_dir, "b9595")
|
||||
monkeypatch.setattr(upd, "_find_binary", lambda: binary)
|
||||
monkeypatch.setattr(upd, "_installer_script", lambda: tmp_path / "install_llama_prebuilt.py")
|
||||
monkeypatch.setattr(
|
||||
freshness,
|
||||
"_fetch_latest_release_tag",
|
||||
lambda repo, timeout = 5.0: "b9596-mix-e6f2453",
|
||||
)
|
||||
|
||||
def _on_start(cmd):
|
||||
_write_install(install_dir, "b9596", release_tag = "b9596-mix-e6f2453")
|
||||
|
||||
_patch_installer_popen(monkeypatch, on_start = _on_start)
|
||||
|
||||
res = upd.start_update()
|
||||
assert res["started"] is True
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
job = upd.get_update_status()["job"]
|
||||
if job["state"] in ("success", "error"):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert job["state"] == "success", job
|
||||
assert job["to_tag"] == "b9596-mix-e6f2453"
|
||||
assert "Updated llama.cpp to b9596-mix-e6f2453." in job["message"]
|
||||
|
||||
|
||||
def test_start_update_installer_failure_reports_error(monkeypatch, tmp_path):
|
||||
install_dir = tmp_path / "llama.cpp"
|
||||
binary = _write_install(install_dir, "b9493")
|
||||
|
|
@ -623,7 +644,7 @@ def test_start_update_installer_missing_refuses(monkeypatch, tmp_path):
|
|||
|
||||
|
||||
class _FakeBackend:
|
||||
"""Minimal stand-in for LlamaCppBackend's update-coordination surface."""
|
||||
"""Fake backend for update coordination."""
|
||||
|
||||
def __init__(self):
|
||||
import threading
|
||||
|
|
@ -659,7 +680,6 @@ def test_update_sets_maintenance_flag_and_unloads(monkeypatch, tmp_path):
|
|||
seen = {}
|
||||
|
||||
def _on_start(cmd):
|
||||
# The maintenance flag must be set while the installer runs.
|
||||
seen["flag_during_install"] = backend._llama_update_in_progress
|
||||
_write_install(install_dir, "b9518")
|
||||
|
||||
|
|
@ -674,8 +694,8 @@ def test_update_sets_maintenance_flag_and_unloads(monkeypatch, tmp_path):
|
|||
time.sleep(0.05)
|
||||
|
||||
assert backend.unloaded is True
|
||||
assert upd.get_update_status()["job"]["reload_required"] is True
|
||||
assert seen.get("flag_during_install") is True
|
||||
# Cleared in the finally so model loads work again after the swap.
|
||||
assert backend._llama_update_in_progress is False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -70,10 +70,11 @@ def test_status_response_exposes_source_build():
|
|||
"installed_at_utc": None,
|
||||
"age_days": None,
|
||||
"source_build": True,
|
||||
"job": {"state": "idle"},
|
||||
"job": {"state": "idle", "reload_required": False},
|
||||
}
|
||||
model = rl.LlamaUpdateStatusResponse(**payload)
|
||||
assert model.model_dump()["source_build"] is True
|
||||
assert model.model_dump()["job"]["reload_required"] is False
|
||||
# Extra/unknown keys must not crash the response model.
|
||||
rl.LlamaUpdateStatusResponse(**{**payload, "unexpected": 1})
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ _job: dict = {
|
|||
"message": "",
|
||||
"from_tag": None,
|
||||
"to_tag": None,
|
||||
"reload_required": None,
|
||||
"error": None,
|
||||
"progress": None,
|
||||
"started_at": None,
|
||||
|
|
@ -416,8 +417,7 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
backend = None
|
||||
model_was_active = False
|
||||
try:
|
||||
# Maintenance state so no load starts a server from the half-swapped binary
|
||||
# (and the old binary is freed for the swap). Fails open without a backend.
|
||||
# Block loads and free the binary while the installer swaps it.
|
||||
try:
|
||||
from routes.inference import get_llama_cpp_backend
|
||||
backend = get_llama_cpp_backend()
|
||||
|
|
@ -431,8 +431,7 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
try:
|
||||
with backend._serial_load_lock:
|
||||
backend._llama_update_in_progress = True
|
||||
# is_active covers the loading/unhealthy window is_loaded misses
|
||||
# (a live process also locks the exe on Windows during the swap).
|
||||
# Active processes can lock the exe on Windows.
|
||||
if getattr(backend, "is_active", False):
|
||||
model_was_active = True
|
||||
backend.unload_model()
|
||||
|
|
@ -451,8 +450,7 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
]
|
||||
cmd.extend(_rocm_install_args(asset))
|
||||
logger.info("llama update: installing", cmd = " ".join(cmd))
|
||||
# Stream the installer output so download percent lines feed
|
||||
# job["progress"]; finer milestones via UNSLOTH_PROGRESS_PERCENT_STEP.
|
||||
# Stream progress lines into job["progress"].
|
||||
env = dict(os.environ, UNSLOTH_PROGRESS_PERCENT_STEP = "5")
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
|
|
@ -493,19 +491,15 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
tail = "".join(tail_lines).strip()[-1500:]
|
||||
raise RuntimeError(f"installer exited {returncode}: {tail or 'no output'}")
|
||||
|
||||
# New UNSLOTH_PREBUILT_INFO.json is on disk; drop the in-memory AND the
|
||||
# on-disk freshness caches, then re-prime the 24h disk cache with the
|
||||
# true newest, so the banner can't linger on a stale same-base value
|
||||
# after the swap. drop_disk matters when the refresh below can't reach
|
||||
# GitHub: without it, latest_published_release would replay the stale
|
||||
# disk value; with it, latest reads as None and the banner fails open.
|
||||
# Drop stale caches so the banner re-checks the swapped marker.
|
||||
# If GitHub is offline, latest stays unknown and the banner fails open.
|
||||
reset_caches(drop_disk = True)
|
||||
try:
|
||||
latest_published_release(repo, force_refresh = True)
|
||||
except Exception as exc: # pragma: no cover - network defensive
|
||||
logger.debug("llama update: post-install freshness refresh failed", error = str(exc))
|
||||
new_marker = read_install_marker(_find_binary())
|
||||
new_tag = (new_marker or {}).get("tag") or (new_marker or {}).get("release_tag")
|
||||
new_tag = (new_marker or {}).get("release_tag") or (new_marker or {}).get("tag")
|
||||
|
||||
with _job_lock:
|
||||
_job.update(
|
||||
|
|
@ -515,6 +509,7 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
+ (" Reload your model to use it." if model_was_active else "")
|
||||
),
|
||||
to_tag = new_tag,
|
||||
reload_required = model_was_active,
|
||||
error = None,
|
||||
progress = 1.0,
|
||||
finished_at = _utcnow(),
|
||||
|
|
@ -530,7 +525,7 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path
|
|||
finished_at = _utcnow(),
|
||||
)
|
||||
finally:
|
||||
# Lift the maintenance state so model loads work again, success or not.
|
||||
# Always clear maintenance state.
|
||||
if backend is not None:
|
||||
try:
|
||||
backend._llama_update_in_progress = False
|
||||
|
|
@ -618,6 +613,7 @@ def start_update() -> dict:
|
|||
message = "Downloading and installing the latest llama.cpp prebuilt...",
|
||||
from_tag = from_tag,
|
||||
to_tag = None,
|
||||
reload_required = None,
|
||||
error = None,
|
||||
progress = 0.0,
|
||||
started_at = _utcnow(),
|
||||
|
|
@ -643,6 +639,7 @@ def _reset_job_for_tests() -> None:
|
|||
message = "",
|
||||
from_tag = None,
|
||||
to_tag = None,
|
||||
reload_required = None,
|
||||
error = None,
|
||||
progress = None,
|
||||
started_at = None,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ import { toast } from "@/lib/toast";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { Download } from "lucide-react";
|
||||
import { type ReactElement, useEffect, useRef, useState } from "react";
|
||||
// Backend progress is coarse (5% steps, ~0.9 max) and the extract tail emits no
|
||||
// signal. Creep toward this cap so the bar keeps moving rather than freezing.
|
||||
// Creep toward this cap between coarse backend progress updates.
|
||||
const RUNNING_CAP = 0.95;
|
||||
|
||||
// Smoothed 0..1 bar progress: eases toward real `progress`, trickles toward a
|
||||
// ceiling when idle, animates to 100% when `done`. Resets to 0 on each start.
|
||||
// Smooth coarse backend progress without freezing between milestones.
|
||||
function useSmoothedProgress(
|
||||
active: boolean,
|
||||
progress: number | null,
|
||||
|
|
@ -35,8 +33,7 @@ function useSmoothedProgress(
|
|||
let raf = 0;
|
||||
let last = performance.now();
|
||||
const tick = (now: number) => {
|
||||
// rAF timestamps can predate the performance.now() captured above, so
|
||||
// clamp dt at 0 to keep the first frame from stepping backwards.
|
||||
// Guard against a first rAF timestamp before the captured start time.
|
||||
const dt = Math.max(0, Math.min((now - last) / 1000, 0.1));
|
||||
last = now;
|
||||
const current = displayRef.current;
|
||||
|
|
@ -74,18 +71,11 @@ function useSmoothedProgress(
|
|||
|
||||
interface LlamaUpdateBannerProps {
|
||||
enabled?: boolean;
|
||||
// false: fill the parent instead of self-anchoring, so banners can stack in a
|
||||
// shared container. true (default) keeps standalone desktop mounts working.
|
||||
// false fills a shared stack; true self-anchors.
|
||||
positioned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-invasive "Update llama.cpp" affordance. Appears bottom-right ~1s after a
|
||||
* newer prebuilt is detected and stays up until the user explicitly acts on it
|
||||
* (X, Update, or Remind me later). Clicking Update swaps the prebuilt in place
|
||||
* via POST /api/llama/update. Can be turned off entirely in Settings ->
|
||||
* General -> Notifications (on by default).
|
||||
*/
|
||||
/** Bottom-right llama.cpp update toast. */
|
||||
export function LlamaUpdateBanner({
|
||||
enabled = true,
|
||||
positioned = true,
|
||||
|
|
@ -99,9 +89,11 @@ export function LlamaUpdateBanner({
|
|||
async function handleUpdate() {
|
||||
const result = await apply();
|
||||
if (result?.ok) {
|
||||
toast.success(
|
||||
`llama.cpp updated to ${result.tag ?? "the latest build"}. Reload your model to use it.`,
|
||||
);
|
||||
const updatedTag = result.tag ?? status?.latest_tag ?? "the latest build";
|
||||
const reloadHint = result.reloadRequired
|
||||
? " Reload your model to use it."
|
||||
: "";
|
||||
toast.success(`llama.cpp updated to ${updatedTag}.${reloadHint}`);
|
||||
} else if (result) {
|
||||
toast.error(
|
||||
`llama.cpp update failed: ${result.error ?? "unknown error"}`,
|
||||
|
|
@ -112,24 +104,20 @@ export function LlamaUpdateBanner({
|
|||
const show =
|
||||
visible && status != null && (status.update_available || applying);
|
||||
const sizeBytes = status?.update_size_bytes ?? null;
|
||||
// Round to whole MB; these prebuilts are hundreds of MB.
|
||||
const sizeLabel =
|
||||
sizeBytes && sizeBytes > 0
|
||||
? `${Math.round(sizeBytes / (1024 * 1024))} MB`
|
||||
: null;
|
||||
const updateProgress = status?.job.progress ?? null;
|
||||
const jobSucceeded = status?.job.state === "success";
|
||||
// Drives the bar so it animates continuously; aria reports the real value.
|
||||
// Display value animates; aria uses the real progress.
|
||||
const displayProgress = useSmoothedProgress(
|
||||
applying,
|
||||
updateProgress,
|
||||
jobSucceeded,
|
||||
);
|
||||
|
||||
// Render with no enter/exit animation. An opacity/transform transition (in or
|
||||
// out) promotes a GPU compositing layer whose creation or teardown can flash
|
||||
// for a frame on real displays, which reads as a flicker on appear and on
|
||||
// dismiss. A plain conditional mount appears and leaves cleanly.
|
||||
// Avoid opacity/transform transitions; GPU layer churn can flash.
|
||||
return show ? (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -220,7 +208,7 @@ export function LlamaUpdateBanner({
|
|||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
// -mr optically aligns the filled pill's edge with the card padding
|
||||
// Align pill edge with card padding.
|
||||
className="-mr-1 h-auto rounded-full px-3.5 py-2 text-[13px]"
|
||||
onClick={handleUpdate}
|
||||
data-testid="llama-update-button"
|
||||
|
|
|
|||
|
|
@ -505,8 +505,7 @@ export function ChatSettingsPanel({
|
|||
(s) => s.loadedSpeculativeType,
|
||||
);
|
||||
const specFallbackReason = useChatRuntimeStore((s) => s.specFallbackReason);
|
||||
// "binary_no_mtp" / "binary_outdated" mean a newer prebuilt would re-enable
|
||||
// MTP; "runtime_error" means the current build cannot run it (no update push).
|
||||
// Only binary fallback states are solved by a newer prebuilt.
|
||||
const mtpUpdatable =
|
||||
specFallbackReason === "binary_no_mtp" ||
|
||||
specFallbackReason === "binary_outdated";
|
||||
|
|
@ -518,8 +517,11 @@ export function ChatSettingsPanel({
|
|||
const handleMtpUpdate = useCallback(async () => {
|
||||
const result = await applyLlamaUpdate();
|
||||
if (result.ok) {
|
||||
const reloadHint = result.reloadRequired
|
||||
? " Reload your model to enable MTP."
|
||||
: "";
|
||||
toast.success(
|
||||
`llama.cpp updated to ${result.tag ?? "the latest build"}. Reload your model to enable MTP.`,
|
||||
`llama.cpp updated to ${result.tag ?? "the latest build"}.${reloadHint}`,
|
||||
);
|
||||
} else {
|
||||
toast.error(`llama.cpp update failed: ${result.error ?? "unknown error"}`);
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@ import { authFetch, getAuthToken } from "@/features/auth";
|
|||
import { refreshHardwareInfo } from "@/hooks/use-hardware-info";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
// First check shortly after load, then re-surface as an hourly reminder. The
|
||||
// banner stays up until the user explicitly acts on it (X, Update, or
|
||||
// Remind me later).
|
||||
// Initial check plus hourly reminders until dismissed or applied.
|
||||
const FIRST_CHECK_DELAY_MS = 1000;
|
||||
const REMINDER_INTERVAL_MS = 60 * 60 * 1000; // ~1 hour
|
||||
// "Remind me later" re-surfaces sooner than the hourly reminder.
|
||||
// Snooze checks sooner than the hourly reminder.
|
||||
const SNOOZE_DELAY_MS = 15 * 60 * 1000; // ~15 minutes
|
||||
// Poll cadence while applying. Short so the installer's ~5% milestones are
|
||||
// observed instead of a fast download finishing between two slow polls.
|
||||
// Poll fast enough to catch installer progress milestones.
|
||||
const JOB_POLL_INTERVAL_MS = 500;
|
||||
|
||||
export interface LlamaUpdateJob {
|
||||
|
|
@ -21,8 +18,9 @@ export interface LlamaUpdateJob {
|
|||
message: string;
|
||||
from_tag: string | null;
|
||||
to_tag: string | null;
|
||||
reload_required: boolean | null;
|
||||
error: string | null;
|
||||
// Download fraction (0..1) while running, 1 on success, null when unknown.
|
||||
// Download fraction while running, 1 on success.
|
||||
progress: number | null;
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +29,7 @@ export interface LlamaUpdateStatus {
|
|||
update_available: boolean;
|
||||
installed_tag: string | null;
|
||||
latest_tag: string | null;
|
||||
// Download size of the prebuilt Update would fetch, in bytes (null if unknown).
|
||||
// Prebuilt download size in bytes, if known.
|
||||
update_size_bytes: number | null;
|
||||
job: LlamaUpdateJob;
|
||||
}
|
||||
|
|
@ -52,6 +50,8 @@ function parseStatus(value: unknown): LlamaUpdateStatus | null {
|
|||
message: typeof job.message === "string" ? job.message : "",
|
||||
from_tag: typeof job.from_tag === "string" ? job.from_tag : null,
|
||||
to_tag: typeof job.to_tag === "string" ? job.to_tag : null,
|
||||
reload_required:
|
||||
typeof job.reload_required === "boolean" ? job.reload_required : null,
|
||||
error: typeof job.error === "string" ? job.error : null,
|
||||
progress: typeof job.progress === "number" ? job.progress : null,
|
||||
},
|
||||
|
|
@ -73,9 +73,7 @@ async function fetchStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Update probes force a refresh so a newly published build is not masked by the
|
||||
// backend's 24h release cache (the banner would otherwise lag up to a day). The
|
||||
// job-progress poll below stays cached; it only reads local job state.
|
||||
// Manual checks bypass the 24h release cache; job polls read local state.
|
||||
const recheckStatus = () => fetchStatus(true);
|
||||
|
||||
interface UseLlamaUpdateCheckOptions {
|
||||
|
|
@ -85,16 +83,11 @@ interface UseLlamaUpdateCheckOptions {
|
|||
export interface LlamaApplyResult {
|
||||
ok: boolean;
|
||||
tag?: string | null;
|
||||
reloadRequired?: boolean | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the backend for a newer llama.cpp prebuilt. When one exists, `visible`
|
||||
* becomes true ~1s after load and stays up until the user dismisses it (X),
|
||||
* snoozes it ("Remind me later", ~15 min), or updates; it re-surfaces every
|
||||
* ~hour as a reminder. `apply()` triggers the in-place swap and tracks the
|
||||
* job.
|
||||
*/
|
||||
/** Tracks llama.cpp update visibility and apply progress. */
|
||||
export function useLlamaUpdateCheck({
|
||||
enabled = true,
|
||||
}: UseLlamaUpdateCheckOptions = {}) {
|
||||
|
|
@ -111,8 +104,7 @@ export function useLlamaUpdateCheck({
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Poll the job to completion. Shared by apply() and surfaceIfAvailable() so a
|
||||
// job is tracked once whoever noticed it; onDone resolves with the result.
|
||||
// Used by apply() and another-tab job tracking.
|
||||
const startJobPoll = useCallback(
|
||||
(onDone?: (result: LlamaApplyResult) => void) => {
|
||||
clearPollTimer();
|
||||
|
|
@ -126,13 +118,15 @@ export function useLlamaUpdateCheck({
|
|||
if (s.job.state === "success") {
|
||||
setVisible(false);
|
||||
void refreshHardwareInfo();
|
||||
onDone?.({ ok: true, tag: s.job.to_tag });
|
||||
onDone?.({
|
||||
ok: true,
|
||||
tag: s.job.to_tag,
|
||||
reloadRequired: s.job.reload_required,
|
||||
});
|
||||
} else if (s.job.state === "error") {
|
||||
// Leave the banner up so the user can retry; clearing applying drops
|
||||
// the "Updating..." state.
|
||||
// Keep the banner visible so retry is available.
|
||||
onDone?.({ ok: false, error: s.job.error });
|
||||
} else {
|
||||
// idle without a terminal result (job reset): stop tracking.
|
||||
onDone?.({ ok: false, error: "update did not complete" });
|
||||
}
|
||||
}, JOB_POLL_INTERVAL_MS);
|
||||
|
|
@ -140,14 +134,12 @@ export function useLlamaUpdateCheck({
|
|||
[clearPollTimer],
|
||||
);
|
||||
|
||||
// Surface the banner when an update is available; it stays up until dismissed.
|
||||
const surfaceIfAvailable = useCallback(
|
||||
(next: LlamaUpdateStatus | null) => {
|
||||
if (!next) return;
|
||||
setStatus(next);
|
||||
if (next.job.state === "running") {
|
||||
// Swap in progress (e.g. another tab): keep the banner up and track the
|
||||
// job so "Updating..." clears when it finishes instead of sticking.
|
||||
// Another tab is applying; show progress here too.
|
||||
setApplying(true);
|
||||
setVisible(true);
|
||||
if (!pollTimer.current) startJobPoll();
|
||||
|
|
@ -162,9 +154,7 @@ export function useLlamaUpdateCheck({
|
|||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
// Disabled mid-update: stop showing and tracking, and clear `applying`
|
||||
// so the banner's animation loop stops too. Re-enabling re-detects a
|
||||
// still-running job below and resumes tracking via surfaceIfAvailable.
|
||||
// Re-enabling will rediscover any still-running job.
|
||||
setVisible(false);
|
||||
setApplying(false);
|
||||
return;
|
||||
|
|
@ -199,7 +189,6 @@ export function useLlamaUpdateCheck({
|
|||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
// Hide now, re-check and re-surface after SNOOZE_DELAY_MS.
|
||||
const snooze = useCallback(() => {
|
||||
setVisible(false);
|
||||
if (snoozeTimer.current) clearTimeout(snoozeTimer.current);
|
||||
|
|
@ -234,9 +223,7 @@ export function useLlamaUpdateCheck({
|
|||
return { ok: false, error: String(e) };
|
||||
}
|
||||
|
||||
// 200 without a started job (no marker / installer missing) leaves it idle,
|
||||
// so surface the reason instead of polling forever. already_running is the
|
||||
// exception: a job is in flight, so track it to completion below.
|
||||
// Non-started jobs stay idle; already_running is tracked below.
|
||||
if (
|
||||
action &&
|
||||
action.started === false &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue