diff --git a/static/js/admin.js b/static/js/admin.js
index 25e3faa..5019096 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -731,14 +731,14 @@ function initEndpointForm() {
urlInput.addEventListener('input', () => {
if (provider.value && urlInput.value.trim() !== provider.value) {
provider.value = '';
- if (kindSel) kindSel.value = 'proxy';
+ if (kindSel) kindSel.value = 'api';
_renderPickerMenu();
_syncPickerCurrent();
}
});
- if (kindSel) kindSel.value = provider.value ? 'api' : (kindSel.value || 'proxy');
+ if (kindSel) kindSel.value = kindSel.value || 'api';
function _apiEndpointKind() {
- return (kindSel && kindSel.value) ? kindSel.value : (provider.value ? 'api' : 'proxy');
+ return (kindSel && kindSel.value) ? kindSel.value : 'api';
}
function _normalizeBaseUrl(raw) {
let u = raw.trim();
diff --git a/static/js/documentLibrary.js b/static/js/documentLibrary.js
index 0341594..642a91f 100644
--- a/static/js/documentLibrary.js
+++ b/static/js/documentLibrary.js
@@ -1598,7 +1598,11 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
modal.innerHTML = `
-
Library
+
+
Library
@@ -1831,6 +1835,27 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
grid.parentElement.appendChild(btn);
}
+ // SVG markup + label for each tab — used to keep the modal header
+ // in sync with whichever sub-tab the user is on.
+ const _TAB_HEADERS = {
+ chats: {
+ label: 'Chats',
+ svg: '',
+ },
+ documents: {
+ label: 'Documents',
+ svg: '',
+ },
+ research: {
+ label: 'Research',
+ svg: '',
+ },
+ archive: {
+ label: 'Archive',
+ svg: '',
+ },
+ };
+
function _switchLibTab(tab) {
_activeLibTab = tab;
_tabBtns.forEach(b => b.classList.toggle('active', b.dataset.doclibTab === tab));
@@ -1841,6 +1866,14 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
p.style.display = 'none';
}
});
+ // Sync the modal header icon + label to match the active sub-tab.
+ const hdr = _TAB_HEADERS[tab];
+ if (hdr) {
+ const ico = document.getElementById('doclib-header-icon');
+ const txt = document.getElementById('doclib-header-text');
+ if (ico) ico.innerHTML = hdr.svg;
+ if (txt) txt.textContent = hdr.label;
+ }
if (tab === 'chats') _renderLibChats();
else if (tab === 'archive') _renderLibArchive();
else if (tab === 'research') _renderLibResearch();
@@ -3121,8 +3154,10 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
return new Date(iso).toLocaleDateString();
}
- // Switch to initial tab if not documents
- if (_activeLibTab !== 'documents') _switchLibTab(_activeLibTab);
+ // Switch to the initial tab. Always call this — even when the
+ // default ('documents') matches — so the modal header's icon + label
+ // sync from "Library" to the active sub-tab on first open.
+ _switchLibTab(_activeLibTab);
const searchInput = document.getElementById('doclib-search');
searchInput.addEventListener('input', () => {
diff --git a/static/js/modelPicker.js b/static/js/modelPicker.js
index 00874ed..07a1766 100644
--- a/static/js/modelPicker.js
+++ b/static/js/modelPicker.js
@@ -179,14 +179,23 @@ function _initModelPickerDropdown() {
const result = [];
const seen = new Set();
items.forEach(item => {
- if (item.offline) return;
+ // Previously: offline endpoints were skipped entirely, so a server
+ // that briefly went down disappeared from the picker — confusing
+ // when the user can still see it (offline-tagged) in Settings.
+ // Now: include offline-endpoint models too but flag them
+ // `stale: true` so the row renderer dims them + shows the offline
+ // pill. The user can still click and try anyway (matches the
+ // existing "local server appears offline" path on line 301).
+ const epOffline = !!item.offline;
const allModels = (item.models || []).concat(item.models_extra || []);
const allDisplay = (item.models_display || []).concat(item.models_extra_display || []);
// Mark local endpoints whose live probe failed.
const probeResult = item.endpoint_id ? _localProbe[item.endpoint_id] : null;
const isLocalDead = !!(probeResult && probeResult.alive === false);
allModels.forEach((mid, i) => {
- // Deduplicate by model ID — prefer DB endpoints over env-discovered
+ // Deduplicate by model ID — prefer ONLINE endpoint entries over
+ // offline duplicates so the user gets a working endpoint first
+ // when the same model is exposed by both.
if (seen.has(mid)) return;
seen.add(mid);
result.push({
@@ -201,8 +210,11 @@ function _initModelPickerDropdown() {
item.host || '',
item.url || '',
].filter(Boolean).join(' '),
- stale: isLocalDead,
- staleReason: isLocalDead ? (probeResult.error || 'not responding') : '',
+ stale: isLocalDead || epOffline,
+ staleReason: epOffline
+ ? (item.ping_error || 'endpoint offline')
+ : (isLocalDead ? (probeResult.error || 'not responding') : ''),
+ offline: epOffline,
});
});
});
@@ -377,22 +389,35 @@ function _initModelPickerDropdown() {
return;
}
- // ── Browse mode: Recent (auto) + Favorites (manual). No flat "All" dump. ──
+ // ── Browse mode: Favorites (manual) + Recent (auto), with dedupe. ──
+ // Rules:
+ // 1. Never list the same model twice in the dropdown. Favorites
+ // win over Recent (if you favorited it, that's where it
+ // belongs — Recent shouldn't show it again as duplicate).
+ // 2. Small catalogs (≤ BROWSE_ALL_LIMIT total) skip the Recent
+ // section entirely — when there's only ~10 models, the whole
+ // list fits below as "All models" and a separate Recent
+ // section just duplicates rows.
const shown = new Set();
- const recentModels = _loadRecent()
- .map(id => byId.get(id))
- .filter(Boolean)
- .slice(0, RECENT_MAX);
const favModels = favs.map(id => byId.get(id)).filter(Boolean);
-
- if (recentModels.length) {
- _addSection('Recent');
- recentModels.forEach(m => { shown.add(m.mid); _addRow(m); });
- }
if (favModels.length) {
_addSection('Favorites');
favModels.forEach(m => { shown.add(m.mid); _addRow(m); });
}
+ // Recent: only render when the catalog is big enough that surfacing
+ // a recency shortlist is actually useful, AND only models that
+ // aren't already in Favorites (dedupe).
+ if (all.length > BROWSE_ALL_LIMIT) {
+ const recentModels = _loadRecent()
+ .map(id => byId.get(id))
+ .filter(Boolean)
+ .filter(m => !shown.has(m.mid))
+ .slice(0, RECENT_MAX);
+ if (recentModels.length) {
+ _addSection('Recent');
+ recentModels.forEach(m => { shown.add(m.mid); _addRow(m); });
+ }
+ }
// Small catalogs: still list everything so users aren't forced to search.
if (all.length <= BROWSE_ALL_LIMIT) {
diff --git a/static/js/slashCommands.js b/static/js/slashCommands.js
index 4e528f6..0f3a720 100644
--- a/static/js/slashCommands.js
+++ b/static/js/slashCommands.js
@@ -4815,7 +4815,15 @@ async function _cmdSetup(args, ctx) {
} else {
pendingSetupProvider = provider;
setupMode = 'endpoint-key-for-provider';
- await _setupReply(`Paste your ${provider.name} API key.`);
+ // Show the canonical "/setup " usage so the user
+ // learns the one-shot form instead of relying on the pasted-key
+ // mode that always greets them with a generic prompt.
+ // _setupReply renders as plain text (no HTML) — use markdown
+ // backticks for the inline code instead of + <>.
+ const _slug = (topic || '').toLowerCase();
+ await _setupReply(
+ `Paste your ${provider.name} API key, or run \`/setup ${_slug} \` to set it in one step.`
+ );
}
return true;
}
@@ -5538,7 +5546,31 @@ const COMMANDS = {
category: 'Getting started',
help: 'Add local or API model endpoints',
handler: _cmdSetup,
- usage: '/setup local URL · /setup groq KEY · /setup copilot · /setup endpoint'
+ usage: '/setup local URL · /setup groq KEY · /setup copilot · /setup endpoint',
+ // Provider subs so the autocomplete popup surfaces "/setup deepseek",
+ // "/setup openai", etc. when the user types "/setup de". Each sub's
+ // handler is a thin wrapper that re-prepends the sub name and
+ // re-dispatches into _cmdSetup, which already knows how to handle
+ // bare-provider (prompts for the key) AND provider-with-key (saves it).
+ // Without the explicit handler, the slash-dispatcher errors with
+ // "subDef.handler is not a function".
+ subs: {
+ deepseek: { help: 'DeepSeek', usage: '/setup deepseek sk-...', handler: (a, c) => _cmdSetup(['deepseek', ...a], c) },
+ openai: { help: 'OpenAI', usage: '/setup openai sk-proj-...', handler: (a, c) => _cmdSetup(['openai', ...a], c) },
+ anthropic: { help: 'Anthropic', usage: '/setup anthropic sk-ant-...',handler: (a, c) => _cmdSetup(['anthropic', ...a], c) },
+ openrouter: { help: 'OpenRouter', usage: '/setup openrouter sk-or-...',handler: (a, c) => _cmdSetup(['openrouter', ...a], c) },
+ groq: { help: 'Groq', usage: '/setup groq gsk_...', handler: (a, c) => _cmdSetup(['groq', ...a], c) },
+ gemini: { help: 'Google Gemini', alias: ['google'], usage: '/setup gemini AIza...', handler: (a, c) => _cmdSetup(['gemini', ...a], c) },
+ xai: { help: 'xAI (Grok)', alias: ['grok'], usage: '/setup xai xai-...', handler: (a, c) => _cmdSetup(['xai', ...a], c) },
+ ollama: { help: 'Ollama Cloud', usage: '/setup ollama KEY', handler: (a, c) => _cmdSetup(['ollama', ...a], c) },
+ copilot: { help: 'GitHub Copilot', usage: '/setup copilot', handler: (a, c) => _cmdSetup(['copilot', ...a], c) },
+ local: { help: 'Local model server (vLLM / LM Studio / llama.cpp / Ollama)',
+ usage: '/setup local http://localhost:8000/v1',
+ handler: (a, c) => _cmdSetup(['local', ...a], c) },
+ endpoint: { help: 'Open the endpoint manager in Settings',
+ usage: '/setup endpoint',
+ handler: (a, c) => _cmdSetup(['endpoint', ...a], c) },
+ },
},
demo: {
alias: ['tour'],