mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-08 22:48:46 -04:00
Merge verified Odysseus fixes
This commit is contained in:
+290
-108
@@ -3,9 +3,9 @@
|
||||
|
||||
import Storage from './storage.js';
|
||||
import uiModule, { autoResize, styledPrompt } from './ui.js';
|
||||
import chatRenderer from './chatRenderer.js';
|
||||
import chatRenderer from './chatRenderer.js?v=20260722ctxheader1';
|
||||
import { providerLogo } from './providers.js';
|
||||
import { initModelPicker, updateModelPicker } from './modelPicker.js';
|
||||
import { initModelPicker, updateModelPicker } from './modelPicker.js?v=20260722ctxheader1';
|
||||
import themeModule from './theme.js';
|
||||
import spinnerModule from './spinner.js';
|
||||
|
||||
@@ -16,6 +16,7 @@ let currentSessionId = null;
|
||||
let _sessionNavToken = 0;
|
||||
let _skipAutoSelect = false;
|
||||
let _suppressNextSessionLoading = false;
|
||||
let _rootFreshChatApplied = false;
|
||||
const HISTORY_DISPLAY_CHAR_LIMIT = 160000;
|
||||
const HISTORY_DISPLAY_TAIL_CHARS = 20000;
|
||||
const HISTORY_PAGE_LIMIT_MOBILE = 8;
|
||||
@@ -26,12 +27,29 @@ const FOLDER_MAX_VISIBLE = 5;
|
||||
let _showAllSessions = false;
|
||||
let _expandedFolders = {}; // folderName -> true if "show more" clicked
|
||||
let _sortMode = Storage.get('odysseus-session-sort') || 'active'; // default to last active
|
||||
const DATE_SECTION_COLLAPSE_KEY = 'ody-session-date-section-collapsed';
|
||||
let _autoCreateInProgress = false; // guard against recursive auto-create
|
||||
const _INCOGNITO_SESSIONS_KEY = 'ody-incognito-sessions'; // sessionStorage key for incognito session IDs
|
||||
const _isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
||||
const _mod = _isMac ? '⌘' : 'Ctrl';
|
||||
let _historyPager = null;
|
||||
|
||||
function _shouldPreserveStartupComposer(msgInput) {
|
||||
if (!msgInput || !msgInput.value) return false;
|
||||
if (window.__odysseusComposerUserEdited) return true;
|
||||
return !!document.getElementById('app-loader') && document.activeElement === msgInput;
|
||||
}
|
||||
|
||||
function _clearComposerUnlessStartupTyped(msgInput) {
|
||||
if (!msgInput) return;
|
||||
if (_shouldPreserveStartupComposer(msgInput)) {
|
||||
msgInput.disabled = false;
|
||||
autoResize(msgInput);
|
||||
return;
|
||||
}
|
||||
msgInput.value = '';
|
||||
}
|
||||
|
||||
function _paintSessionLoading(chatHistory, label = 'Loading chat') {
|
||||
if (!chatHistory) return;
|
||||
if (chatRenderer.hideWelcomeScreen) chatRenderer.hideWelcomeScreen();
|
||||
@@ -973,21 +991,68 @@ function _sessionBucketDate(s) {
|
||||
return s.last_message_at || s.updated_at || s.created_at || '';
|
||||
}
|
||||
|
||||
function _loadDateSectionCollapseState() {
|
||||
const raw = Storage.getJSON(DATE_SECTION_COLLAPSE_KEY, {});
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
||||
return raw;
|
||||
}
|
||||
|
||||
function _saveDateSectionCollapseState(state) {
|
||||
Storage.setJSON(DATE_SECTION_COLLAPSE_KEY, state && typeof state === 'object' ? state : {});
|
||||
}
|
||||
|
||||
function _dateSectionKey(kind, label) {
|
||||
return `${kind || 'session'}:${label || 'Older'}`;
|
||||
}
|
||||
|
||||
function _isDateSectionCollapsed(kind, label) {
|
||||
return _loadDateSectionCollapseState()[_dateSectionKey(kind, label)] === true;
|
||||
}
|
||||
|
||||
function _toggleDateSection(kind, label) {
|
||||
const state = _loadDateSectionCollapseState();
|
||||
const key = _dateSectionKey(kind, label);
|
||||
state[key] = state[key] !== true;
|
||||
_saveDateSectionCollapseState(state);
|
||||
renderSessionList();
|
||||
}
|
||||
|
||||
function _createDateSectionHeader(label, kind = 'session') {
|
||||
const el = document.createElement('div');
|
||||
el.className = `date-section-header ${kind}-date-section-header`;
|
||||
el.textContent = label;
|
||||
const collapsed = _isDateSectionCollapsed(kind, label);
|
||||
if (collapsed) el.classList.add('collapsed');
|
||||
el.dataset.dateSectionKind = kind;
|
||||
el.dataset.dateSectionLabel = label;
|
||||
el.tabIndex = 0;
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
el.title = collapsed ? `Show ${label}` : `Hide ${label}`;
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
_toggleDateSection(kind, label);
|
||||
});
|
||||
el.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
_toggleDateSection(kind, label);
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function _appendSessionItemsWithDateHeaders(frag, items) {
|
||||
let lastLabel = null;
|
||||
let collapsed = false;
|
||||
for (const s of items) {
|
||||
const label = _dateBucketLabel(_sessionBucketDate(s));
|
||||
if (label !== lastLabel) {
|
||||
frag.appendChild(_createDateSectionHeader(label, 'session'));
|
||||
collapsed = _isDateSectionCollapsed('session', label);
|
||||
lastLabel = label;
|
||||
}
|
||||
if (collapsed) continue;
|
||||
frag.appendChild(createSessionItem(s));
|
||||
}
|
||||
}
|
||||
@@ -995,6 +1060,7 @@ function _appendSessionItemsWithDateHeaders(frag, items) {
|
||||
function _appendFavoriteSessionItems(frag, items) {
|
||||
if (!items.length) return;
|
||||
frag.appendChild(_createDateSectionHeader('Favorites', 'session'));
|
||||
if (_isDateSectionCollapsed('session', 'Favorites')) return;
|
||||
for (const s of items) {
|
||||
frag.appendChild(createSessionItem(s));
|
||||
}
|
||||
@@ -1226,9 +1292,7 @@ function _renderSessionListImpl() {
|
||||
visibleFolder.push(folderSessions[activeInFolder]);
|
||||
}
|
||||
|
||||
visibleFolder.forEach(s => {
|
||||
content.appendChild(createSessionItem(s));
|
||||
});
|
||||
_appendSessionItemsWithDateHeaders(content, visibleFolder);
|
||||
|
||||
if (folderSessions.length > FOLDER_MAX_VISIBLE) {
|
||||
const rem = folderSessions.length - FOLDER_MAX_VISIBLE;
|
||||
@@ -1328,9 +1392,7 @@ function _renderSessionListImpl() {
|
||||
}
|
||||
|
||||
if (unfiledTarget) {
|
||||
visibleUnfiled.forEach(s => {
|
||||
unfiledTarget.appendChild(createSessionItem(s));
|
||||
});
|
||||
_appendSessionItemsWithDateHeaders(unfiledTarget, visibleUnfiled);
|
||||
}
|
||||
|
||||
// "Show more" / "Show less" toggle
|
||||
@@ -1616,7 +1678,11 @@ export async function loadSessions() {
|
||||
sessionStorage.removeItem('ody-prefetch-sessions');
|
||||
fetched = JSON.parse(prefetched);
|
||||
} else {
|
||||
const res = await fetch(`${API_BASE}/api/sessions`);
|
||||
let url = `${API_BASE}/api/sessions`;
|
||||
if (currentSessionId && _isIncognitoSession(currentSessionId)) {
|
||||
url += `?active_incognito_id=${encodeURIComponent(currentSessionId)}`;
|
||||
}
|
||||
const res = await fetch(url);
|
||||
fetched = await res.json();
|
||||
}
|
||||
sessions = _normalizeSessionsList(fetched);
|
||||
@@ -1640,7 +1706,13 @@ export async function loadSessions() {
|
||||
if (/^(document|note|image|email|event|task|skill|research)-/.test(hashId) || /^open=notes¬e=/.test(hashId)) {
|
||||
hashId = '';
|
||||
}
|
||||
let savedId = Storage.get('lastSessionId');
|
||||
const _isFirstLoad = !sessionStorage.getItem('ody-session-active');
|
||||
const _freshRootLoad = !_rootFreshChatApplied && !hashId && !currentSessionId && !_pendingChat;
|
||||
if (_freshRootLoad) {
|
||||
_rootFreshChatApplied = true;
|
||||
Storage.remove('lastSessionId');
|
||||
}
|
||||
let savedId = _freshRootLoad ? null : Storage.get('lastSessionId');
|
||||
// If the persisted lastSessionId points to a transient session (legacy
|
||||
// state from before the persistence-guard was added), drop it.
|
||||
if (savedId) {
|
||||
@@ -1665,13 +1737,13 @@ export async function loadSessions() {
|
||||
} else if (currentSessionId) {
|
||||
// Session was just created but may not be in the list yet — keep it
|
||||
targetId = currentSessionId;
|
||||
} else if (savedId && activeSessions.some(s => s.id === savedId)) {
|
||||
} else if (!_freshRootLoad && savedId && activeSessions.some(s => s.id === savedId)) {
|
||||
targetId = savedId;
|
||||
} else if (!_skipAutoSelect && _realSessions.length > 0) {
|
||||
} else if (!_freshRootLoad && !_skipAutoSelect && _realSessions.length > 0) {
|
||||
// Most-recent NON-transient session — skip Assistant / Tasks so the
|
||||
// auto-firing assistant doesn't become the apparent default chat.
|
||||
targetId = _realSessions[0].id;
|
||||
} else if (!_skipAutoSelect && activeSessions.length > 0) {
|
||||
} else if (!_freshRootLoad && !_skipAutoSelect && activeSessions.length > 0) {
|
||||
// Only transient sessions exist (brand-new account) — fall through to
|
||||
// the original behaviour so we don't leave the user with nothing.
|
||||
targetId = activeSessions[0].id;
|
||||
@@ -1687,31 +1759,15 @@ export async function loadSessions() {
|
||||
// picker would still show the old model's name from cached state). See
|
||||
// the targetId resolution above (hash → currentSession → lastSessionId →
|
||||
// most-recent).
|
||||
const _isFirstLoad = !sessionStorage.getItem('ody-session-active');
|
||||
if (_isFirstLoad) {
|
||||
sessionStorage.setItem('ody-session-active', '1');
|
||||
if (!targetId) {
|
||||
try {
|
||||
const dcRes = await fetch(`${API_BASE}/api/default-chat`);
|
||||
const dc = await dcRes.json();
|
||||
if (dc.endpoint_url && dc.model) {
|
||||
// Check if there's already an empty session with this model we can reuse
|
||||
const emptyDefault = activeSessions.find(s =>
|
||||
s.model === dc.model && s.message_count === 0
|
||||
);
|
||||
if (emptyDefault) {
|
||||
targetId = emptyDefault.id;
|
||||
} else {
|
||||
await createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id);
|
||||
// On mobile, hide sidebar so user lands directly in chat
|
||||
if (window.innerWidth < 768) {
|
||||
const sb = document.getElementById('sidebar');
|
||||
if (sb) sb.classList.add('hidden');
|
||||
}
|
||||
return; // createDirectChat handles selectSession internally
|
||||
}
|
||||
}
|
||||
} catch (_) { /* no default model configured */ }
|
||||
if (_isFirstLoad) sessionStorage.setItem('ody-session-active', '1');
|
||||
if ((_isFirstLoad || _freshRootLoad) && !targetId) {
|
||||
// Land on a visually fresh chat without creating hidden pending session
|
||||
// state. The send path can create the default-backed session when the
|
||||
// user actually submits. Pre-creating here races with opening an existing
|
||||
// chat and was causing sends to jump into brand-new chats.
|
||||
if (window.innerWidth < 768) {
|
||||
const sb = document.getElementById('sidebar');
|
||||
if (sb) sb.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1743,10 +1799,9 @@ export async function loadSessions() {
|
||||
if (activeSessions.length === 0 && !_autoCreateInProgress) {
|
||||
_autoCreateInProgress = true;
|
||||
try {
|
||||
const dcRes = await fetch(`${API_BASE}/api/default-chat`);
|
||||
const dc = await dcRes.json();
|
||||
if (dc.endpoint_url && dc.model) {
|
||||
await createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id);
|
||||
const dc = await _getPreferredDefaultChat();
|
||||
if (dc && dc.endpoint_url && dc.model) {
|
||||
await createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default' });
|
||||
}
|
||||
} catch (_) { /* no default model — that's fine, user can /setup */ }
|
||||
_autoCreateInProgress = false;
|
||||
@@ -1767,6 +1822,13 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
try {
|
||||
const navToken = ++_sessionNavToken;
|
||||
const prevSessionId = currentSessionId;
|
||||
// Selecting a real persisted chat cancels any deferred "New Chat" model
|
||||
// pick. Otherwise the next send can materialize that pending chat instead
|
||||
// of posting into the session the user just opened.
|
||||
if (_pendingChat) {
|
||||
_pendingChat = null;
|
||||
_pendingMaterializePromise = null;
|
||||
}
|
||||
_clearHistoryPager();
|
||||
// Re-archive peeked session when navigating away
|
||||
_checkPeekCleanup(id);
|
||||
@@ -1775,6 +1837,7 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
try { window.documentModule.clearSelection(); } catch {}
|
||||
}
|
||||
currentSessionId = id;
|
||||
try { window.__odysseusLastSelectedSessionId = id; } catch (_) {}
|
||||
// Identify Assistant / task-output sessions so we don't "trap" the user
|
||||
// there on return. Skipped from both `lastSessionId` persistence and the
|
||||
// URL hash — the user complained that coming back to Odysseus kept
|
||||
@@ -1784,10 +1847,6 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
const _isTransientChat = !!_meta && (_meta.folder === 'Assistant' || _meta.folder === 'Tasks');
|
||||
if (!_isTransientChat) {
|
||||
Storage.set('lastSessionId', id);
|
||||
// Update URL hash without triggering hashchange handler
|
||||
if (window.location.hash !== '#' + id) {
|
||||
history.replaceState(null, '', '#' + id);
|
||||
}
|
||||
}
|
||||
// Restore character preset for persistent chats
|
||||
try {
|
||||
@@ -1827,7 +1886,7 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
const msgInput = document.getElementById('message');
|
||||
if (msgInput) {
|
||||
msgInput.disabled = false;
|
||||
msgInput.value = '';
|
||||
_clearComposerUnlessStartupTyped(msgInput);
|
||||
msgInput.style.height = '';
|
||||
msgInput.style.overflow = '';
|
||||
autoResize(msgInput);
|
||||
@@ -1859,6 +1918,7 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
}
|
||||
// Update model picker visibility
|
||||
updateModelPicker();
|
||||
if (window.refreshChatContextHeader) window.refreshChatContextHeader('select-session');
|
||||
|
||||
// Refresh session cost badge for the newly selected session
|
||||
if (chatRenderer.updateSessionCostUI) chatRenderer.updateSessionCostUI();
|
||||
@@ -2086,8 +2146,44 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru
|
||||
|
||||
// Pending session — stored locally until the first message is sent
|
||||
let _pendingChat = null; // { url, modelId, endpointId }
|
||||
let _pendingMaterializePromise = null;
|
||||
|
||||
export function createDirectChat(url, modelId, endpointId) {
|
||||
async function _getPreferredDefaultChat() {
|
||||
let dc = null;
|
||||
try {
|
||||
dc = window.__odysseusDefaultChat || null;
|
||||
} catch (_) {}
|
||||
if (!dc || !dc.endpoint_url || !dc.model) {
|
||||
try {
|
||||
dc = JSON.parse(localStorage.getItem('odysseus-default-chat-cache') || 'null');
|
||||
} catch (_) {}
|
||||
}
|
||||
if (dc && dc.endpoint_url && dc.model) return dc;
|
||||
try {
|
||||
const dcRes = await fetch(`${API_BASE}/api/default-chat`);
|
||||
dc = await dcRes.json();
|
||||
if (dc && dc.endpoint_url && dc.model) {
|
||||
try {
|
||||
window.__odysseusDefaultChat = dc;
|
||||
localStorage.setItem('odysseus-default-chat-cache', JSON.stringify(dc));
|
||||
} catch (_) {}
|
||||
return dc;
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createDirectChat(url, modelId, endpointId, opts = {}) {
|
||||
const incomingSource = opts.source || 'manual';
|
||||
if (
|
||||
_pendingChat &&
|
||||
_pendingChat.modelId &&
|
||||
_pendingChat.source === 'manual' &&
|
||||
incomingSource !== 'manual'
|
||||
) {
|
||||
updateModelPicker();
|
||||
return;
|
||||
}
|
||||
_sessionNavToken++;
|
||||
// Detach any active stream so it doesn't interfere with the new chat
|
||||
if (window.chatModule && window.chatModule.detachCurrentStream) {
|
||||
@@ -2101,10 +2197,12 @@ export function createDirectChat(url, modelId, endpointId) {
|
||||
}
|
||||
|
||||
// Don't hit the API — just store the model info and prepare the UI
|
||||
_pendingChat = { url, modelId, endpointId };
|
||||
_pendingChat = { url, modelId, endpointId, source: incomingSource };
|
||||
_pendingMaterializePromise = null;
|
||||
_skipAutoSelect = true;
|
||||
_suppressNextSessionLoading = true;
|
||||
currentSessionId = null;
|
||||
try { window.__odysseusLastSelectedSessionId = ''; } catch (_) {}
|
||||
Storage.remove('lastSessionId');
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
document.querySelectorAll('.list-item.active-session, .session-item.active').forEach(el => {
|
||||
@@ -2132,6 +2230,7 @@ export function createDirectChat(url, modelId, endpointId) {
|
||||
|
||||
// Update model picker to show the pending model
|
||||
updateModelPicker();
|
||||
if (window.refreshChatContextHeader) window.refreshChatContextHeader('new-chat');
|
||||
|
||||
// Update current-meta header
|
||||
const metaEl = document.getElementById('current-meta');
|
||||
@@ -2141,69 +2240,111 @@ export function createDirectChat(url, modelId, endpointId) {
|
||||
|
||||
// Enable input
|
||||
const msgInput = document.getElementById('message');
|
||||
if (msgInput) { msgInput.disabled = false; msgInput.value = ''; msgInput.focus(); }
|
||||
if (msgInput) {
|
||||
msgInput.disabled = false;
|
||||
_clearComposerUnlessStartupTyped(msgInput);
|
||||
msgInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** Actually create the session in the DB. Called on first message send. */
|
||||
export async function materializePendingSession() {
|
||||
if (_pendingMaterializePromise) return _pendingMaterializePromise;
|
||||
const pending = _pendingChat;
|
||||
if (!pending) return false;
|
||||
_pendingChat = null;
|
||||
|
||||
const materializePromise = (async () => {
|
||||
|
||||
const incognitoChk = document.getElementById('incognito-toggle');
|
||||
const isIncognito = incognitoChk && incognitoChk.checked;
|
||||
const base = (pending.modelId || 'model').split('/').pop();
|
||||
const name = isIncognito ? 'Nobody' : `${base} ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('endpoint_url', pending.url || '');
|
||||
fd.append('model', pending.modelId || '');
|
||||
if (pending.url && pending.modelId) {
|
||||
fd.append('skip_validation', 'true');
|
||||
}
|
||||
if (pending.endpointId) {
|
||||
fd.append('endpoint_id', pending.endpointId);
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: fd });
|
||||
} catch (e) {
|
||||
uiModule.showError('Failed to reach backend: ' + e);
|
||||
return false;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await res.json();
|
||||
} catch {
|
||||
payload = { detail: await res.text() };
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
uiModule.showError(`Session create failed (${res.status}) ${payload.detail || JSON.stringify(payload)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user may have opened an existing chat while this deferred default
|
||||
// session was being created. Do not let a stale response steal
|
||||
// currentSessionId and make the next send land in a brand-new chat.
|
||||
if (_pendingChat !== pending) {
|
||||
if (payload.id) {
|
||||
fetch(`${API_BASE}/api/session/${encodeURIComponent(payload.id)}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIncognito && payload.id) {
|
||||
_markIncognito(payload.id);
|
||||
}
|
||||
|
||||
// Clear any leftover document text selection from the previous session
|
||||
if (window.documentModule?.clearSelection) {
|
||||
try { window.documentModule.clearSelection(); } catch {}
|
||||
}
|
||||
_pendingChat = null;
|
||||
currentSessionId = payload.id;
|
||||
if (!isIncognito) {
|
||||
Storage.set('lastSessionId', payload.id);
|
||||
}
|
||||
|
||||
// Reload the sidebar in the background. Awaiting this used to block the first
|
||||
// prompt in a new/pending chat behind startup fetches and slow /api/sessions
|
||||
// calls, so the user's message could sit for 20s+ before streaming began.
|
||||
_suppressNextSessionLoading = true;
|
||||
if (window.refreshChatContextHeader) window.refreshChatContextHeader('materialize-session');
|
||||
loadSessions().catch(() => {});
|
||||
return true;
|
||||
})();
|
||||
_pendingMaterializePromise = materializePromise;
|
||||
|
||||
try {
|
||||
return await materializePromise;
|
||||
} finally {
|
||||
if (_pendingMaterializePromise === materializePromise) {
|
||||
_pendingMaterializePromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function preMaterializePendingSession() {
|
||||
if (!_pendingChat || _pendingMaterializePromise) return;
|
||||
const incognitoChk = document.getElementById('incognito-toggle');
|
||||
const isIncognito = incognitoChk && incognitoChk.checked;
|
||||
const base = (pending.modelId || 'model').split('/').pop();
|
||||
const name = isIncognito ? 'Nobody' : `${base} ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('endpoint_url', pending.url || '');
|
||||
fd.append('model', pending.modelId || '');
|
||||
if (pending.url && pending.modelId) {
|
||||
fd.append('skip_validation', 'true');
|
||||
}
|
||||
if (pending.endpointId) {
|
||||
fd.append('endpoint_id', pending.endpointId);
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: fd });
|
||||
} catch (e) {
|
||||
uiModule.showError('Failed to reach backend: ' + e);
|
||||
return false;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await res.json();
|
||||
} catch {
|
||||
payload = { detail: await res.text() };
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
uiModule.showError(`Session create failed (${res.status}) ${payload.detail || JSON.stringify(payload)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIncognito && payload.id) {
|
||||
_markIncognito(payload.id);
|
||||
}
|
||||
|
||||
// Clear any leftover document text selection from the previous session
|
||||
if (window.documentModule?.clearSelection) {
|
||||
try { window.documentModule.clearSelection(); } catch {}
|
||||
}
|
||||
currentSessionId = payload.id;
|
||||
Storage.set('lastSessionId', payload.id);
|
||||
history.replaceState(null, '', '#' + payload.id);
|
||||
|
||||
// Reload the sidebar in the background. Awaiting this used to block the first
|
||||
// prompt in a new/pending chat behind startup fetches and slow /api/sessions
|
||||
// calls, so the user's message could sit for 20s+ before streaming began.
|
||||
_suppressNextSessionLoading = true;
|
||||
loadSessions().catch(() => {});
|
||||
return true;
|
||||
if (incognitoChk && incognitoChk.checked) return;
|
||||
setTimeout(() => {
|
||||
const chk = document.getElementById('incognito-toggle');
|
||||
if (chk && chk.checked) return;
|
||||
if (_pendingChat && !_pendingMaterializePromise) {
|
||||
materializePendingSession().catch(() => {});
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
export function hasPendingChat() { return !!_pendingChat; }
|
||||
@@ -2213,6 +2354,10 @@ export function getCurrentSessionId() {
|
||||
return currentSessionId;
|
||||
}
|
||||
|
||||
export function isCurrentSessionIncognito() {
|
||||
return !!(currentSessionId && _isIncognitoSession(currentSessionId));
|
||||
}
|
||||
|
||||
export function getSessions() {
|
||||
return sessions;
|
||||
}
|
||||
@@ -2220,9 +2365,8 @@ export function getSessions() {
|
||||
export function getCurrentModel() {
|
||||
const sess = sessions.find(x => x.id === currentSessionId);
|
||||
if (sess && sess.model) return sess.model;
|
||||
// Pending session not yet materialized — read from model picker label
|
||||
const label = document.getElementById('model-picker-label');
|
||||
return label ? label.textContent.trim() : null;
|
||||
if (_pendingChat && _pendingChat.modelId) return _pendingChat.modelId;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Endpoint URL serving the current (or pending) session's model. Used to
|
||||
@@ -2237,6 +2381,7 @@ export function getCurrentEndpointUrl() {
|
||||
export function setCurrentSessionId(id) {
|
||||
_sessionNavToken++;
|
||||
currentSessionId = id;
|
||||
try { window.__odysseusLastSelectedSessionId = id || ''; } catch (_) {}
|
||||
if (!id) {
|
||||
_suppressNextSessionLoading = true;
|
||||
Storage.remove('lastSessionId');
|
||||
@@ -2247,6 +2392,41 @@ export function setCurrentSessionId(id) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCurrentSessionFromTopMenu() {
|
||||
const sid = currentSessionId;
|
||||
if (!sid) {
|
||||
uiModule.showToast('No chat to delete');
|
||||
return false;
|
||||
}
|
||||
const session = sessions.find(s => String(s.id) === String(sid));
|
||||
if (session?.is_important) {
|
||||
uiModule.showToast('Unfavorite before deleting');
|
||||
return false;
|
||||
}
|
||||
if (!await uiModule.styledConfirm('Delete this session?', { confirmText: 'Delete', danger: true })) {
|
||||
return false;
|
||||
}
|
||||
if (window.chatModule && window.chatModule.abortCurrentRequest) {
|
||||
window.chatModule.abortCurrentRequest();
|
||||
}
|
||||
_deselectCurrentSession(sid);
|
||||
_removeSessionFromLocalState(sid);
|
||||
_skipAutoSelect = true;
|
||||
try {
|
||||
const pm = await import('./presets.js');
|
||||
if (pm.removePersistentChat) pm.removePersistentChat(sid);
|
||||
} catch (e) {}
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/session/${sid}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
uiModule.showToast('Session deleted');
|
||||
} catch (e) {
|
||||
uiModule.showError('Failed to delete session');
|
||||
}
|
||||
await loadSessions();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Session list keyboard navigation: arrows to move, Delete to delete
|
||||
async function _onSessionListKeydown(e) {
|
||||
const item = e.target.closest('.list-item[data-session-id]');
|
||||
@@ -3454,6 +3634,7 @@ const sessionModule = {
|
||||
selectSession,
|
||||
createDirectChat,
|
||||
materializePendingSession,
|
||||
preMaterializePendingSession,
|
||||
hasPendingChat,
|
||||
getPendingChat,
|
||||
getCurrentSessionId,
|
||||
@@ -3475,7 +3656,8 @@ const sessionModule = {
|
||||
closeArchive,
|
||||
setSessionHasDocs,
|
||||
getSortMode,
|
||||
setSortMode
|
||||
setSortMode,
|
||||
deleteCurrentSessionFromTopMenu
|
||||
};
|
||||
|
||||
export { updateModelPicker };
|
||||
|
||||
Reference in New Issue
Block a user