diff --git a/static/js/chat.js b/static/js/chat.js
index 9bc582b41..272d2bc16 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -1541,9 +1541,9 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
-
+
- `;
@@ -1589,13 +1589,15 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
_liveThinkTimerEl.textContent = _formatThinkStats(_elapsedLive, _liveThinkTokenCount);
}
// Keep thinking box scrolled to bottom, but let user scroll up
+ var _followThinking = true;
var thinkBox = _liveThinkInner.closest('.thinking-content');
if (thinkBox) {
var nearBottom = thinkBox.scrollHeight - thinkBox.clientHeight - thinkBox.scrollTop < 80;
if (nearBottom) thinkBox.scrollTop = thinkBox.scrollHeight;
+ _followThinking = nearBottom;
}
}
- uiModule.scrollHistory();
+ if (_followThinking) uiModule.scrollHistory();
continue;
} else if (!hasUnclosedThink && isThinking) {
isThinking = false;
diff --git a/static/js/cookbook-hwfit.js b/static/js/cookbook-hwfit.js
index 9e5cda02d..e22b75b74 100644
--- a/static/js/cookbook-hwfit.js
+++ b/static/js/cookbook-hwfit.js
@@ -561,8 +561,9 @@ function _ollamaToHwfitRows(libModels, vramAvail, ramAvail) {
return out;
}
-export async function _hwfitFetch(fresh = false) {
+export async function _hwfitFetch(fresh = false, opts = {}) {
const _tk = ++_hwfitFetchToken;
+ const allowNetwork = fresh || opts.allowNetwork !== false;
const useCase = document.getElementById('hwfit-usecase')?.value || '';
const search = document.getElementById('hwfit-search')?.value?.trim() || '';
const remoteHost = _envState.remoteHost || '';
@@ -588,6 +589,13 @@ export async function _hwfitFetch(fresh = false) {
}
_hwfitRenderList(list, _applyEngineFilter(_cached.models));
} else {
+ if (!allowNetwork) {
+ _hwfitCache = null;
+ _hwfitRenderHw(hw, null);
+ list.innerHTML = 'No cached scan yet
Press Scan to test hardware and rank models for this server.
';
+ try { wp.destroy(); } catch {}
+ return;
+ }
// Show spinner while scanning — stack the spinner above a text label
// (the .hwfit-loading class is a centered flex ROW, so force column here).
const loadingDiv = document.createElement('div');
@@ -606,6 +614,10 @@ export async function _hwfitFetch(fresh = false) {
list.appendChild(loadingDiv);
_hwfitCache = null; // no instant paint — clear until the fetch returns
}
+ if (!allowNetwork) {
+ try { wp.destroy(); } catch {}
+ return;
+ }
// Only fetch cached model IDs when server changes, not on every search/sort
const remoteKey = _currentServerValue();
if (!_cachedModelIds || _lastCacheHost() !== remoteKey) {
@@ -2168,15 +2180,12 @@ export function _hwfitInit() {
}
});
});
- // Auto-test when host or port blur
+ // Manual connectivity test after editing host or port. Existing saved
+ // servers are not auto-tested on panel open; unreachable hosts can stall the
+ // Cookbook UI and make opening the panel feel blocked.
entry.querySelectorAll('.cookbook-srv-host, .cookbook-srv-port').forEach(el => {
el.addEventListener('blur', () => _testServerConnection(entry));
});
- // Initial test for pre-filled rows (existing servers on tab load)
- if (entry.querySelector('.cookbook-srv-host')?.value?.trim() && !entry.dataset.tested) {
- entry.dataset.tested = '1';
- _testServerConnection(entry);
- }
// Cancel button on a brand-new server entry: discard it (no confirm — it's
// unsaved) and re-sync so the dropped blank server doesn't linger.
const cancelBtn = entry.querySelector('.cookbook-server-cancel-btn');
diff --git a/static/js/cookbook.js b/static/js/cookbook.js
index 3206948c2..6451f4bc0 100644
--- a/static/js/cookbook.js
+++ b/static/js/cookbook.js
@@ -1640,10 +1640,10 @@ function _wireTabEvents(body) {
});
if (backend === 'Search') {
_hwfitInit();
- _hwfitFetch();
+ _hwfitFetch(false, { allowNetwork: false });
}
if (backend === 'Serve') {
- _fetchCachedModels();
+ _fetchCachedModels(false, { allowNetwork: false });
}
if (backend === 'Dependencies') {
_fetchDependencies();
@@ -1746,7 +1746,7 @@ function _wireTabEvents(body) {
_applyServerSelection(dlServer.value);
// Reset toggle state (no flicker) so the new server's hardware re-renders.
_resetGpuToggleState();
- _hwfitFetch();
+ _hwfitFetch(false, { allowNetwork: false });
});
}
@@ -1783,7 +1783,7 @@ function _wireTabEvents(body) {
if (settingsTab) settingsTab.click();
});
}
- _fetchCachedModels();
+ _fetchCachedModels(false, { allowNetwork: false });
});
}
@@ -2863,7 +2863,7 @@ function _renderRecipes() {
// Auto-init What Fits
_hwfitInit();
- _hwfitFetch();
+ _hwfitFetch(false, { allowNetwork: false });
}
// ── Public API ──
diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js
index f2aba5641..42c237efe 100644
--- a/static/js/cookbookRunning.js
+++ b/static/js/cookbookRunning.js
@@ -1862,6 +1862,7 @@ export function _renderRunningTab() {
body.querySelectorAll('.cookbook-group').forEach(g => {
g.classList.toggle('hidden', g.dataset.backendGroup !== 'Running');
});
+ setTimeout(() => _renderRunningTab(), 0);
});
} else if (runTab) {
const _errCount2 = tasks.filter(t => t.status === 'error' || t.status === 'crashed').length;
@@ -2712,7 +2713,7 @@ export function _renderRunningTab() {
// responds; without this, the user opens the Running tab and sees
// only the placeholder ("Launched by scheduled task …") because
// _reconnectTask never fires for status 'ready'/'loading'/'warming'.
- if (['running', 'ready', 'loading', 'warming', 'starting'].includes(task.status)) {
+ if (_isRunningTabVisible() && ['running', 'ready', 'loading', 'warming', 'starting'].includes(task.status)) {
_reconnectTask(el, task);
}
}
@@ -3438,6 +3439,23 @@ async function _reconnectTask(el, task) {
let _bgMonitorInterval = null;
+function _hasLiveTasks(tasks = null) {
+ const list = tasks || _loadTasks();
+ return list.some(t =>
+ t.status === 'running'
+ || t.status === 'queued'
+ || t.status === 'ready'
+ || _downloadOutputLooksActive(t)
+ );
+}
+
+function _isRunningTabVisible() {
+ const modal = document.getElementById('cookbook-modal');
+ if (!modal || modal.classList.contains('hidden')) return false;
+ const activeTab = modal.querySelector('.cookbook-tab.active')?.dataset?.backend || '';
+ return activeTab === 'Running';
+}
+
// Reachability check for running serve tasks. The tmux pane can stay alive
// while the model server inside it has crashed (so no "Process exited" line
// ever appears) — leaving the card showing "running" forever. So we actively
@@ -3656,7 +3674,9 @@ export function _startBackgroundMonitor() {
// crashed/etc. whose tmux session is actually still running, and flip
// them back to running. Internally throttled to 8s so a manual call from
// the open path or a fast invocation doesn't double up.
- _selfHealStaleTasks().catch(() => {});
+ if (_hasLiveTasks() || _isRunningTabVisible()) {
+ _selfHealStaleTasks().catch(() => {});
+ }
}, BG_MONITOR_INTERVAL_MS);
_pollBackgroundStatus();
_checkServeReachability();
@@ -3972,17 +3992,15 @@ export function initRunning(shared) {
_detectModelOptimizations = shared._detectModelOptimizations;
_buildServeCmd = shared._buildServeCmd;
- // App boot: pull authoritative state from server, then auto-start
- // the background monitor unconditionally. Used to gate on "already
- // has running tasks" but that meant when the agent (or anyone)
- // added a task after boot, the UI never noticed. 10s poll of a
- // small status endpoint is cheap and gives the agent + the UI a
- // shared live picture.
+ // App boot: pull authoritative state from server, but don't start the
+ // running-task monitor unless there is real work to watch. Starting it
+ // unconditionally made a plain Cookbook open keep probing stale tmux/SSH
+ // sessions, which is expensive when a saved remote host is unreachable.
(async () => {
try {
await _syncFromServer();
} catch {}
- _startBackgroundMonitor();
+ if (_hasLiveTasks()) _startBackgroundMonitor();
})();
}
diff --git a/static/js/cookbookServe.js b/static/js/cookbookServe.js
index 0e168da60..98af9d0e2 100644
--- a/static/js/cookbookServe.js
+++ b/static/js/cookbookServe.js
@@ -3663,9 +3663,10 @@ function _renderCachedModelsData(list, data, host) {
_rerenderCachedModels();
}
-export async function _fetchCachedModels(fresh = false) {
+export async function _fetchCachedModels(fresh = false, opts = {}) {
const list = document.getElementById('hwfit-cached-list');
if (!list) return;
+ const allowNetwork = fresh || opts.allowNetwork !== false;
list.innerHTML = '';
const _dlWp = spinnerModule.createWhirlpool(22);
@@ -3740,6 +3741,13 @@ export async function _fetchCachedModels(fresh = false) {
_renderCachedModelsData(list, cached, host);
return;
}
+ if (!allowNetwork) {
+ _dlWp.destroy();
+ list.innerHTML = 'No cached model scan yet
Press Scan to check this server\'s model cache.
';
+ const tagContainer = document.getElementById('serve-tags');
+ if (tagContainer) tagContainer.innerHTML = '';
+ return;
+ }
const res = await fetch(`/api/model/cached${params}`);
if (!res.ok) {
const body = await res.text().catch(() => '');
diff --git a/static/js/sessions.js b/static/js/sessions.js
index da2799e35..698991477 100644
--- a/static/js/sessions.js
+++ b/static/js/sessions.js
@@ -2,7 +2,7 @@
// This module handles all session-related operations
import Storage from './storage.js';
-import uiModule, { styledPrompt } from './ui.js';
+import uiModule, { autoResize, styledPrompt } from './ui.js';
import markdownModule from './markdown.js';
import chatRenderer from './chatRenderer.js';
import { providerLogo } from './providers.js';
@@ -1859,6 +1859,9 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
if (msgInput) {
msgInput.disabled = false;
msgInput.value = '';
+ msgInput.style.height = '';
+ msgInput.style.overflow = '';
+ autoResize(msgInput);
}
const sendBtn2 = document.querySelector('.send-btn');
if (sendBtn2) {