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
Thinking\u2026
- + -
+
`; @@ -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) {