From 63ae69ea2966e9da5bbc379159463579c571e7d6 Mon Sep 17 00:00:00 2001
From: Matyas Fenyves <16389204+uhhgoat@users.noreply.github.com>
Date: Wed, 10 Jun 2026 15:36:49 +0200
Subject: [PATCH 1/2] fix(cookbook): prevent duplicate dependency installs
Track active dependency install tasks in the Cookbook dependency list so running installs render as a downloading state instead of another install action.
Refresh dependency state when install tasks finish or are removed after stop/cancel, preventing stale downloading pills in the Dependencies tab.
Fixes #3778
---
static/js/cookbook.js | 103 +++++++++++++++++++++++++++++++++--
static/js/cookbookRunning.js | 9 ++-
static/style.css | 16 ++++++
3 files changed, 121 insertions(+), 7 deletions(-)
diff --git a/static/js/cookbook.js b/static/js/cookbook.js
index 9625cbd3e..bee7bb5b6 100644
--- a/static/js/cookbook.js
+++ b/static/js/cookbook.js
@@ -1067,6 +1067,36 @@ export function _persistEnvState() {
// ── Dependencies ──
// Category colors removed — using theme CSS classes instead
+const _pendingDepInstalls = new Set();
+const _ACTIVE_DEP_STATUSES = new Set(['queued', 'running']);
+
+function _depHostKey(host) {
+ const h = String(host || '').trim();
+ return (!h || h === 'local' || h === 'localhost' || h === '127.0.0.1') ? 'local' : h;
+}
+
+function _depInstallKey(pipName, host = '', port = '', envPath = '') {
+ return [
+ String(pipName || '').trim(),
+ _depHostKey(host),
+ String(port || '').trim(),
+ String(envPath || '').trim(),
+ ].join('\n');
+}
+
+function _taskDepInstallKey(task) {
+ if (!task?.payload?._dep || !task.payload.repo_id) return '';
+ return _depInstallKey(
+ task.payload.repo_id,
+ task.remoteHost || task.payload.remote_host || '',
+ task.sshPort || task.payload.ssh_port || '',
+ task.payload.env_path || task.payload._envPath || ''
+ );
+}
+
+function _isActiveDepTask(task) {
+ return !!task?.payload?._dep && _ACTIVE_DEP_STATUSES.has(task.status || '');
+}
async function _fetchDependencies() {
const list = document.getElementById('cookbook-deps-list');
@@ -1128,9 +1158,31 @@ async function _fetchDependencies() {
if (!pkgs.length) { list.innerHTML = '
No packages found
'; return; }
const _winUnsupported = new Set(['hf_transfer', 'vllm', 'rembg', 'gfpgan']);
const _systemInstallable = new Set(['tmux']);
+ const _activeDepTasks = new Map();
+ _loadTasks().forEach(task => {
+ if (!_isActiveDepTask(task)) return;
+ const key = _taskDepInstallKey(task);
+ if (key && !_activeDepTasks.has(key)) _activeDepTasks.set(key, task);
+ });
+ _pendingDepInstalls.forEach(key => {
+ if (key && !_activeDepTasks.has(key)) _activeDepTasks.set(key, { _pending: true });
+ });
- const _statusTag = (pkg, isLocal, isSystemDep, winBlocked) => {
+ const _depTarget = (isLocal) => isLocal
+ ? { host: '', port: '', envPath: '' }
+ : { host: _depHost, port: _depPort, envPath: _depVenv };
+ const _activeDepFor = (pkg, isLocal) => {
+ if (!pkg?.pip) return null;
+ const target = _depTarget(isLocal);
+ return _activeDepTasks.get(_depInstallKey(pkg.pip, target.host, target.port, target.envPath)) || null;
+ };
+
+ const _statusTag = (pkg, isLocal, isSystemDep, winBlocked, activeDep) => {
if (winBlocked) return `N/A`;
+ if (activeDep) {
+ const session = activeDep.sessionId ? ` (${activeDep.sessionId})` : '';
+ return `downloading`;
+ }
if (pkg.installed && isSystemDep) return `Installed`;
if (pkg.installed && pkg.pip_update_available === false && pkg.name !== 'llama_cpp') {
const tip = esc(pkg.update_note || pkg.status_note || 'Found externally; update outside Odysseus.');
@@ -1174,6 +1226,7 @@ async function _fetchDependencies() {
const isLocal = pkg.target === 'local';
const isSystemDep = pkg.kind === 'system';
const winBlocked = !isLocal && _isWindows() && _winUnsupported.has(pkg.name);
+ const activeDep = _activeDepFor(pkg, isLocal);
const note = pkg.status_note ? `${esc(pkg.status_note)}
` : '';
const updateNote = pkg.installed && pkg.pip_update_available === false && pkg.update_note ? `${esc(pkg.update_note)}
` : '';
// Inline rebuild/reinstall tag. Styled as a .cookbook-dep-tag so it
@@ -1183,7 +1236,11 @@ async function _fetchDependencies() {
// diagnosis-style `_launchServeTask` with `pip install --force-reinstall`
// so the user can watch the pip install in the Running tab.
let _rebuildBtn = '';
- if (pkg.name === 'vllm' && pkg.installed) {
+ if (activeDep) {
+ _rebuildBtn = '';
+ } else if (pkg.name === 'llama_cpp') {
+ _rebuildBtn = ``;
+ } else if (pkg.name === 'vllm' && pkg.installed) {
_rebuildBtn = ``;
} else if (pkg.name === 'sglang' && pkg.installed) {
_rebuildBtn = ``;
@@ -1229,7 +1286,7 @@ async function _fetchDependencies() {
+ _rebuildBtn
+ _buildDepsBtn
+ `${esc(pkg.category)}`
- + _statusTag(pkg, isLocal, isSystemDep, winBlocked)
+ + _statusTag(pkg, isLocal, isSystemDep, winBlocked, activeDep)
+ recipeCaret
+ ``
+ recipePanel;
@@ -1417,6 +1474,28 @@ async function _fetchDependencies() {
}
const targetPlatform = isLocalOnly ? (_envState.hostPlatform || _envState.platform || '') : (targetServer?.platform || _envState.platform || '');
const targetRemoteHost = isLocalOnly ? '' : (targetServer?.host || _envState.remoteHost || '');
+ const activeKey = _depInstallKey(
+ pipName,
+ targetRemoteHost || '',
+ isLocalOnly ? '' : (_getPort(targetRemoteHost) || ''),
+ targetEnvPath || ''
+ );
+ if (_pendingDepInstalls.has(activeKey) || _loadTasks().some(task => _isActiveDepTask(task) && _taskDepInstallKey(task) === activeKey)) {
+ uiModule.showToast(`${pkgName} is already downloading on ${targetHost}.`);
+ if (statusEl) {
+ statusEl.textContent = 'downloading';
+ statusEl.disabled = true;
+ statusEl.classList.add('cookbook-dep-downloading');
+ }
+ return;
+ }
+ _pendingDepInstalls.add(activeKey);
+ if (statusEl) {
+ statusEl.textContent = 'downloading';
+ statusEl.disabled = true;
+ statusEl.classList.add('cookbook-dep-downloading');
+ statusEl.title = `${pkgName} is already downloading`;
+ }
// Always go through `python -m pip` so the leading token is `python`
// — matches the /api/model/serve allow-list (bare `pip` is blocked).
// Inside a venv/conda env, `--user` is invalid (pip refuses), so we
@@ -1488,13 +1567,20 @@ async function _fetchDependencies() {
action: 'OK',
onAction: () => {},
});
+ _pendingDepInstalls.delete(activeKey);
+ if (statusEl) {
+ statusEl.textContent = upgrade ? 'Update' : 'Install';
+ statusEl.disabled = false;
+ statusEl.classList.remove('cookbook-dep-downloading');
+ statusEl.title = '';
+ }
return;
}
// _dep flags this as a pip dependency/driver install (not a servable
// model) so the running-task card doesn't offer a "Serve →" button.
- const payload = { repo_id: depTaskId, _cmd: cmd, remote_host: targetRemoteHost || '', _dep: true, env_path: targetEnvPath || '', platform: targetPlatform || '' };
+ const payload = { repo_id: depTaskId, _cmd: cmd, remote_host: targetRemoteHost || '', ssh_port: isLocalOnly ? '' : (_getPort(targetRemoteHost) || ''), _dep: true, _dep_action: upgrade ? 'update' : 'install', env_path: targetEnvPath || '', platform: targetPlatform || '' };
_addTask(data.session_id, 'pip ' + pkgName, 'download', payload);
- if (statusEl) { statusEl.textContent = upgrade ? 'Updating...' : 'Installing...'; statusEl.disabled = true; }
+ _pendingDepInstalls.delete(activeKey);
uiModule.showToast(`${upgrade ? 'Updating' : 'Installing'} ${pkgName} on ${targetHost}...`);
} catch (err) {
uiModule.showToast('Install failed: ' + err.message, {
@@ -1502,6 +1588,13 @@ async function _fetchDependencies() {
action: 'OK',
onAction: () => {},
});
+ _pendingDepInstalls.delete(activeKey);
+ if (statusEl) {
+ statusEl.textContent = upgrade ? 'Update' : 'Install';
+ statusEl.disabled = false;
+ statusEl.classList.remove('cookbook-dep-downloading');
+ statusEl.title = '';
+ }
}
}
diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js
index 5057d40d5..c6ad4767d 100644
--- a/static/js/cookbookRunning.js
+++ b/static/js/cookbookRunning.js
@@ -969,6 +969,9 @@ function _updateTask(sessionId, updates) {
if (uptime) uptime.style.display = 'none';
}
}
+ if (task?.type === 'download' && task.payload?._dep && updates.status && !['queued', 'running'].includes(task.status || '')) {
+ _refreshDepsAfterInstall(task);
+ }
}
function _refreshDepsAfterInstall(task) {
@@ -980,8 +983,10 @@ function _refreshDepsAfterInstall(task) {
export function _removeTask(sessionId) {
_tombstoneTask(sessionId); // so sync/poll can't resurrect it
- const tasks = _loadTasks().filter(t => t.sessionId !== sessionId);
- _saveTasks(tasks);
+ const tasks = _loadTasks();
+ const task = tasks.find(t => t.sessionId === sessionId);
+ _saveTasks(tasks.filter(t => t.sessionId !== sessionId));
+ _refreshDepsAfterInstall(task);
_renderRunningTab();
}
diff --git a/static/style.css b/static/style.css
index 73fdbcd5b..928c4e98b 100644
--- a/static/style.css
+++ b/static/style.css
@@ -20632,6 +20632,14 @@ body.gallery-selecting .gallery-dl-btn,
padding: 0 10px;
box-sizing: border-box;
}
+.cookbook-dep-downloading {
+ background: color-mix(in srgb, var(--orange, #ffb86c) 18%, transparent);
+ color: var(--orange, #ffb86c);
+ border: 1px solid color-mix(in srgb, var(--orange, #ffb86c) 35%, transparent);
+ min-width: 75.85px;
+ padding: 0 10px;
+ box-sizing: border-box;
+}
.cookbook-dep-na {
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: color-mix(in srgb, var(--fg) 60%, transparent);
@@ -20660,6 +20668,13 @@ body.gallery-selecting .gallery-dl-btn,
-webkit-appearance: none;
-moz-appearance: none;
}
+.cookbook-dep-install.cookbook-dep-downloading {
+ background: color-mix(in srgb, var(--orange, #ffb86c) 18%, transparent);
+ color: var(--orange, #ffb86c);
+ border: 1px solid color-mix(in srgb, var(--orange, #ffb86c) 35%, transparent);
+ cursor: default;
+ opacity: 1;
+}
/* Conditional line under the Download h2: only when the section is folded
(collapsed). When expanded, the body content provides separation; the
underline reads as clutter. */
@@ -20693,6 +20708,7 @@ body.gallery-selecting .gallery-dl-btn,
padding-left: 0.5px;
}
.cookbook-dep-install:hover { opacity: 0.85; }
+.cookbook-dep-install.cookbook-dep-downloading:hover { opacity: 1; }
/* Installed split button: "Installed" label + separator + ▾ caret; clicking it
opens the actions menu (Update). Replaces the old ⋮ button. */
.cookbook-dep-installed-btn {
From 2376bb7ce203e9257047c5f0d719f2851dd959e0 Mon Sep 17 00:00:00 2001
From: Matyas Fenyves <16389204+uhhgoat@users.noreply.github.com>
Date: Fri, 31 Jul 2026 14:01:59 +0200
Subject: [PATCH 2/2] chore(ci): refresh PR checks