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:
+390
-98
@@ -6,29 +6,29 @@ import Storage from './js/storage.js';
|
||||
import uiModule from './js/ui.js';
|
||||
import workspaceModule from './js/workspace.js';
|
||||
import fileHandlerModule from './js/fileHandler.js';
|
||||
import modelsModule from './js/models.js';
|
||||
import modelsModule from './js/models.js?v=20260715startupcalm2';
|
||||
import ragModule from './js/rag.js';
|
||||
import presetsModule from './js/presets.js';
|
||||
import searchModule from './js/search.js';
|
||||
import chatModule from './js/chat.js';
|
||||
import compareModule from './js/compare/index.js';
|
||||
import documentModule from './js/document.js';
|
||||
import chatModule from './js/chat.js?v=20260722ctxheader4';
|
||||
import compareModule from './js/compare/index.js?v=20260723compareicon2';
|
||||
import documentModule from './js/document.js?v=20260722emailfastindex1';
|
||||
import searchChatModule from './js/search-chat.js';
|
||||
import { makeWindowDraggable } from './js/windowDrag.js';
|
||||
import markdownModule from './js/markdown.js';
|
||||
import chatRenderer from './js/chatRenderer.js';
|
||||
import sessionModule from './js/sessions.js';
|
||||
import memoryModule from './js/memory.js';
|
||||
import chatRenderer from './js/chatRenderer.js?v=20260722emailfastindex1';
|
||||
import sessionModule from './js/sessions.js?v=20260722ctxheader4';
|
||||
import memoryModule from './js/memory.js?v=20260722memoryloading1';
|
||||
import voiceRecorderModule from './js/voiceRecorder.js';
|
||||
import censorModule from './js/censor.js';
|
||||
import galleryModule from './js/gallery.js';
|
||||
import tasksModule from './js/tasks.js?v=20260630tasksactivity';
|
||||
import tasksModule from './js/tasks.js?v=20260723tasksbulkfeedback1';
|
||||
import calendarModule from './js/calendar.js';
|
||||
import notesModule from './js/notes.js';
|
||||
import adminModule from './js/admin.js';
|
||||
import settingsModule from './js/settings.js';
|
||||
import adminModule from './js/admin.js?v=20260716openrouter3';
|
||||
import settingsModule from './js/settings.js?v=20260722emailfastindex1';
|
||||
// Eagerly bind unified minimize/restore behavior across all tool modals.
|
||||
import './js/modalManager.js';
|
||||
import './js/modalManager.js?v=20260723compareicon2';
|
||||
// Desktop window tiling — drag a modal near an edge/corner to snap.
|
||||
import './js/tileManager.js';
|
||||
import themeModule from './js/theme.js';
|
||||
@@ -43,7 +43,7 @@ import * as researchPanelModule from './js/research/panel.js?v=20260630researcht
|
||||
import ttsModule from './js/tts-ai.js';
|
||||
import spinnerModule from './js/spinner.js';
|
||||
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
|
||||
import { initSidebarLayout, syncRailSide } from './js/sidebar-layout.js';
|
||||
import { initSidebarLayout, syncRailSide } from './js/sidebar-layout.js?v=20260715startupclean';
|
||||
import { initSectionCollapse, initSectionDrag } from './js/section-management.js';
|
||||
|
||||
const API_BASE = window.location.origin;
|
||||
@@ -204,12 +204,22 @@ const el = uiModule.el;
|
||||
// changes take effect immediately (previously cached once at page load and
|
||||
// went stale when the user changed their default model).
|
||||
let _defaultChat = null;
|
||||
try {
|
||||
const cachedDefaultChat = JSON.parse(localStorage.getItem('odysseus-default-chat-cache') || 'null');
|
||||
if (cachedDefaultChat && cachedDefaultChat.endpoint_url && cachedDefaultChat.model) {
|
||||
_defaultChat = cachedDefaultChat;
|
||||
window.__odysseusDefaultChat = cachedDefaultChat;
|
||||
}
|
||||
} catch (_) {}
|
||||
async function _refreshDefaultChat() {
|
||||
try {
|
||||
const d = await (await fetch('/api/default-chat')).json();
|
||||
if (d && d.endpoint_url && d.model) {
|
||||
_defaultChat = d;
|
||||
try { window.__odysseusDefaultChat = d; } catch (_) {}
|
||||
try {
|
||||
window.__odysseusDefaultChat = d;
|
||||
localStorage.setItem('odysseus-default-chat-cache', JSON.stringify(d));
|
||||
} catch (_) {}
|
||||
return d;
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -224,7 +234,7 @@ async function _createDirectChatFromPreferredModel() {
|
||||
|
||||
const pending = sessionModule.getPendingChat && sessionModule.getPendingChat();
|
||||
if (pending && pending.url && pending.modelId && pending.endpointId) {
|
||||
sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId);
|
||||
sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId, { source: pending.source || 'manual' });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -238,7 +248,7 @@ async function _createDirectChatFromPreferredModel() {
|
||||
|
||||
const dc = await _refreshDefaultChat();
|
||||
if (dc) {
|
||||
sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id);
|
||||
sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default' });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -252,50 +262,6 @@ async function _createDirectChatFromPreferredModel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function _hasUsableChatModel() {
|
||||
try {
|
||||
const pending = sessionModule?.getPendingChat?.();
|
||||
if (pending && pending.url && pending.modelId) return true;
|
||||
} catch (_) {}
|
||||
try {
|
||||
const current = sessionModule?.getSessions?.()
|
||||
?.find(s => s.id === sessionModule?.getCurrentSessionId?.());
|
||||
if (current && current.endpoint_url && current.model) return true;
|
||||
} catch (_) {}
|
||||
const dc = await _refreshDefaultChat();
|
||||
if (dc && dc.endpoint_url && dc.model) return true;
|
||||
try {
|
||||
const items = window.modelsModule?.getCachedItems?.() || [];
|
||||
if (items.some(item => !item.offline && ((item.models || []).length || (item.models_extra || []).length))) {
|
||||
return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/models?background=false`, { credentials: 'same-origin' });
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
return (data.items || []).some(item => !item.offline && ((item.models || []).length || (item.models_extra || []).length));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function _syncWelcomeModelHint() {
|
||||
const tip = document.getElementById('welcome-tip');
|
||||
const sub = document.getElementById('welcome-sub');
|
||||
if (!tip && !sub) return;
|
||||
const hasModel = await _hasUsableChatModel();
|
||||
if (hasModel) {
|
||||
if (sub && !sub.dataset.researchOrigText) sub.textContent = 'New chat ready.';
|
||||
if (tip) tip.textContent = 'Pick a model if you want, or just type.';
|
||||
} else {
|
||||
if (sub && !sub.dataset.researchOrigText) {
|
||||
sub.innerHTML = 'Welcome, <span class="setup-trigger-link" style="color:var(--accent,var(--red));font-weight:600;cursor:pointer;text-decoration:underline;" title="Click to launch setup">type /setup</span> to get started.';
|
||||
}
|
||||
if (tip) tip.textContent = 'Add an AI endpoint from Settings in the sidebar, or paste an endpoint/API key into the chat.';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EVENT LISTENERS INITIALIZATION
|
||||
// ============================================
|
||||
@@ -522,6 +488,20 @@ function initializeEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Export menu: Compact current chat context
|
||||
const exportCompactBtn = el('export-compact-btn');
|
||||
if (exportCompactBtn) {
|
||||
exportCompactBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
exportMenu.classList.remove('open');
|
||||
if (window.compactCurrentChatContext) {
|
||||
await window.compactCurrentChatContext();
|
||||
} else {
|
||||
uiModule.showError('Compact action is not ready yet');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export: PDF
|
||||
const exportPdfBtn = el('export-pdf-btn');
|
||||
if (exportPdfBtn) {
|
||||
@@ -574,6 +554,18 @@ function initializeEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Export menu: Delete current chat
|
||||
const exportDeleteBtn = el('export-delete-btn');
|
||||
if (exportDeleteBtn) {
|
||||
exportDeleteBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
exportMenu.classList.remove('open');
|
||||
if (sessionModule?.deleteCurrentSessionFromTopMenu) {
|
||||
await sessionModule.deleteCurrentSessionFromTopMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rename session from top bar
|
||||
const exportRenameBtn = el('export-rename-btn');
|
||||
if (exportRenameBtn) {
|
||||
@@ -1247,6 +1239,7 @@ function initializeEventListeners() {
|
||||
if (libraryNewDocBtn) {
|
||||
libraryNewDocBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (libraryNewDocBtn.dataset.docNewWired === '1') return;
|
||||
try {
|
||||
if (documentModule && documentModule.newDocument) await documentModule.newDocument();
|
||||
} catch (err) {
|
||||
@@ -1746,6 +1739,42 @@ function initializeEventListeners() {
|
||||
uiModule.showToast(`${label} ${active ? 'on' : 'off'}`, 1800);
|
||||
}
|
||||
|
||||
function syncPlanToggle(active) {
|
||||
const btn = el('plan-toggle-btn');
|
||||
const chk = el('plan-toggle');
|
||||
const status = el('plan-mode-status');
|
||||
const statusToggle = el('plan-mode-status-toggle');
|
||||
if (chk) chk.checked = !!active;
|
||||
document.body.classList.toggle('plan-mode-active', !!active);
|
||||
if (status) status.hidden = !active;
|
||||
if (statusToggle) {
|
||||
statusToggle.setAttribute('aria-pressed', String(!!active));
|
||||
statusToggle.setAttribute('aria-label', active ? 'Turn off plan mode' : 'Turn on plan mode');
|
||||
}
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', !!active);
|
||||
btn.setAttribute('aria-pressed', String(!!active));
|
||||
btn.title = active
|
||||
? 'Plan mode on - next message proposes a plan only'
|
||||
: 'Plan mode';
|
||||
}
|
||||
}
|
||||
|
||||
function setPlanMode(active, options = {}) {
|
||||
const on = !!active;
|
||||
const st = loadToggleState();
|
||||
st.plan_mode = on;
|
||||
saveToggleState(st);
|
||||
syncPlanToggle(on);
|
||||
if (on) {
|
||||
const resChk = el('research-toggle');
|
||||
if (resChk && resChk.checked) _syncResearchIndicator(false);
|
||||
}
|
||||
if (!options.silent && uiModule?.showToast) {
|
||||
uiModule.showToast(on ? 'Plan mode on' : 'Plan mode off', 1600);
|
||||
}
|
||||
}
|
||||
|
||||
function applyModeToToggles(mode) {
|
||||
MODE_TOOLS.forEach(({ btnId, checkboxId, stateKey }) => {
|
||||
const btn = el(btnId);
|
||||
@@ -1764,8 +1793,8 @@ function initializeEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Agent / Chat mode toggle ──
|
||||
(function initModeToggle() {
|
||||
// ── Agent / Chat mode toggle ──
|
||||
(function initModeToggle() {
|
||||
const agentBtn = el('mode-agent-btn');
|
||||
const chatBtn = el('mode-chat-btn');
|
||||
if (!agentBtn || !chatBtn) return;
|
||||
@@ -1803,7 +1832,61 @@ function initializeEventListeners() {
|
||||
setMode('agent');
|
||||
});
|
||||
chatBtn.addEventListener('click', () => setMode('chat'));
|
||||
setMode(currentMode);
|
||||
setMode(currentMode);
|
||||
})();
|
||||
|
||||
(function initPlanToggle() {
|
||||
const btn = el('plan-toggle-btn');
|
||||
const state = loadToggleState();
|
||||
syncPlanToggle(!!state.plan_mode);
|
||||
window.__odysseusSetPlanMode = (active) => setPlanMode(active, { silent: true });
|
||||
const statusToggle = el('plan-mode-status-toggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
const st = loadToggleState();
|
||||
setPlanMode(!st.plan_mode);
|
||||
});
|
||||
}
|
||||
if (statusToggle) {
|
||||
statusToggle.addEventListener('click', () => setPlanMode(false));
|
||||
}
|
||||
const msgInput = el('message');
|
||||
if (msgInput && !msgInput._odysseusPlanTabToggle) {
|
||||
msgInput._odysseusPlanTabToggle = true;
|
||||
msgInput.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Tab' || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const st = loadToggleState();
|
||||
setPlanMode(!st.plan_mode);
|
||||
});
|
||||
}
|
||||
const chatBar = document.querySelector('.chat-input-bar');
|
||||
if (chatBar && !chatBar._odysseusPlanSwipeToggle) {
|
||||
chatBar._odysseusPlanSwipeToggle = true;
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchStartAt = 0;
|
||||
chatBar.addEventListener('touchstart', (e) => {
|
||||
if (!e.touches || e.touches.length !== 1) return;
|
||||
const t = e.touches[0];
|
||||
touchStartX = t.clientX;
|
||||
touchStartY = t.clientY;
|
||||
touchStartAt = Date.now();
|
||||
}, { passive: true });
|
||||
chatBar.addEventListener('touchend', (e) => {
|
||||
if (!touchStartAt || !e.changedTouches || e.changedTouches.length !== 1) return;
|
||||
if (!window.matchMedia || !window.matchMedia('(max-width: 768px)').matches) return;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - touchStartX;
|
||||
const dy = t.clientY - touchStartY;
|
||||
const dt = Date.now() - touchStartAt;
|
||||
touchStartAt = 0;
|
||||
if (dt > 700 || Math.abs(dx) < 56 || Math.abs(dy) > 28 || Math.abs(dx) < Math.abs(dy) * 2) return;
|
||||
const st = loadToggleState();
|
||||
setPlanMode(!st.plan_mode);
|
||||
}, { passive: true });
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Tool splash explainer messages (shown first 2 times per tool) ──
|
||||
@@ -2302,21 +2385,43 @@ function initializeEventListeners() {
|
||||
const PLACEHOLDER_COMPACT_WIDTH = 400;
|
||||
const PICKER_HIDE_WIDTH = 220;
|
||||
const TOOLBAR_HIDE_WIDTH = 160;
|
||||
const textarea = el('message');
|
||||
const inputBottom = document.querySelector('.chat-input-bottom');
|
||||
const _isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const textarea = el('message');
|
||||
const inputBottom = document.querySelector('.chat-input-bottom');
|
||||
const _isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
let _placeholderHintOn = false;
|
||||
|
||||
function checkPickerOverflow() {
|
||||
// Skip responsive collapse on mobile — keyboard open/close causes flicker
|
||||
if (_isMobile) return;
|
||||
const w = inputTop.clientWidth;
|
||||
// Hide model picker
|
||||
pickerWrap.classList.toggle('picker-auto-hidden', w < PICKER_HIDE_WIDTH);
|
||||
// Keep a prompt inside the composer even when the picker crowds the row.
|
||||
// A blank placeholder makes the mobile/compact empty state feel broken.
|
||||
if (textarea) {
|
||||
textarea.setAttribute('placeholder', w < PLACEHOLDER_COMPACT_WIDTH ? 'Message...' : 'Message Odysseus...');
|
||||
}
|
||||
function setComposerPlaceholder(width) {
|
||||
if (!textarea) return;
|
||||
if (_isMobile && _placeholderHintOn) {
|
||||
textarea.setAttribute('placeholder', 'Swipe to toggle plan');
|
||||
return;
|
||||
}
|
||||
textarea.setAttribute('placeholder', width < PLACEHOLDER_COMPACT_WIDTH ? 'Message...' : 'Message Odysseus...');
|
||||
}
|
||||
|
||||
if (_isMobile && textarea && !textarea._odysseusPlanPlaceholderHint) {
|
||||
textarea._odysseusPlanPlaceholderHint = true;
|
||||
setInterval(() => {
|
||||
_placeholderHintOn = !_placeholderHintOn;
|
||||
setComposerPlaceholder(inputTop.clientWidth || window.innerWidth || 0);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function checkPickerOverflow() {
|
||||
const w = inputTop.clientWidth || window.innerWidth || 0;
|
||||
const hasText = !!(textarea && textarea.value && textarea.value.trim());
|
||||
if (_isMobile) {
|
||||
// Mobile has much less horizontal room: any typed text should get
|
||||
// the full composer row, matching plan-mode's collision behavior.
|
||||
pickerWrap.classList.toggle('picker-auto-hidden', hasText);
|
||||
setComposerPlaceholder(w);
|
||||
return;
|
||||
}
|
||||
// Hide model picker
|
||||
pickerWrap.classList.toggle('picker-auto-hidden', w < PICKER_HIDE_WIDTH);
|
||||
// Keep a prompt inside the composer even when the picker crowds the row.
|
||||
// A blank placeholder makes the mobile/compact empty state feel broken.
|
||||
setComposerPlaceholder(w);
|
||||
// Hide entire bottom toolbar (tools, mode toggle) — only send button remains
|
||||
if (inputBottom) {
|
||||
inputBottom.classList.toggle('toolbar-auto-hidden', w < TOOLBAR_HIDE_WIDTH);
|
||||
@@ -2482,6 +2587,11 @@ function initializeEventListeners() {
|
||||
incognitoBtn.title = chk.checked ? 'Disable Nobody mode' : 'Enable Nobody mode — no memory, no history saved';
|
||||
const welcomeName = document.querySelector('.welcome-name');
|
||||
if (chk.checked) {
|
||||
try {
|
||||
if (sessionModule && sessionModule.setCurrentSessionId) sessionModule.setCurrentSessionId(null);
|
||||
const box = el('chat-history');
|
||||
if (box) box.innerHTML = '';
|
||||
} catch (_) {}
|
||||
incognitoBtn.innerHTML = INCOGNITO_EYE_CLOSED + '<span class="incognito-label">Nobody</span>';
|
||||
if (welcomeName) {
|
||||
welcomeName.dataset.originalHtml = welcomeName.innerHTML;
|
||||
@@ -2605,10 +2715,9 @@ function initializeEventListeners() {
|
||||
'sidebar-brand': '.sidebar-brand-title',
|
||||
'sidebar-new-chat': '#sidebar-new-chat-btn',
|
||||
'sidebar-search': '#sidebar-search-btn',
|
||||
'sessions-section': '#sessions-section',
|
||||
'email-section': '#email-section',
|
||||
'models-section': '#models-section',
|
||||
'tools-section': '#tools-section',
|
||||
'sessions-section': '#sessions-section',
|
||||
'email-section': '#email-section',
|
||||
'tools-section': '#tools-section',
|
||||
// Per-tool visibility — fine-grained control over which entries show
|
||||
// inside the Tools section in the sidebar.
|
||||
'tool-calendar': '#tool-calendar-btn',
|
||||
@@ -2639,7 +2748,7 @@ function initializeEventListeners() {
|
||||
};
|
||||
|
||||
// Keys hidden by default on first run (no localStorage yet)
|
||||
const UI_VIS_DEFAULT_OFF = new Set(['models-section', 'rag-toggle-btn', 'text-emojis', 'chat-fullwidth']);
|
||||
const UI_VIS_DEFAULT_OFF = new Set(['rag-toggle-btn', 'text-emojis', 'chat-fullwidth']);
|
||||
|
||||
// Keys that need admin to toggle off (reserved for future use)
|
||||
const UI_VIS_ADMIN_ONLY = new Set([]);
|
||||
@@ -3160,7 +3269,7 @@ function initializeEventListeners() {
|
||||
['.memory-tabs', '.memory-tab'],
|
||||
['.admin-tabs', '.admin-tab'],
|
||||
];
|
||||
const _IGNORE = 'input, textarea, select, [contenteditable="true"], .preset-range, ' +
|
||||
const _IGNORE = '.chat-input-bar, input, textarea, select, [contenteditable="true"], .preset-range, ' +
|
||||
'.note-cl-row, .minimized-dock-chip, canvas, .email-card-reader';
|
||||
let sx = 0, sy = 0, tracking = false;
|
||||
|
||||
@@ -3576,6 +3685,7 @@ function initializeEventListeners() {
|
||||
// INITIALIZATION ON PAGE LOAD
|
||||
// ============================================
|
||||
function startOdysseusApp() {
|
||||
tasksModule?.startNotificationPolling?.();
|
||||
if (window.__odysseusAppStarted) return;
|
||||
window.__odysseusAppStarted = true;
|
||||
const _bumpChatPriority = (ms = 10000) => {
|
||||
@@ -3787,6 +3897,7 @@ function startOdysseusApp() {
|
||||
return;
|
||||
}
|
||||
|
||||
chatRenderer.hideWelcomeScreen();
|
||||
return originalSubmit.call(chatModule, e);
|
||||
}
|
||||
|
||||
@@ -3797,6 +3908,86 @@ function startOdysseusApp() {
|
||||
const messageInput = el('message');
|
||||
const modelPickerWrap = document.getElementById('model-picker-wrap');
|
||||
|
||||
function _readComposerPromptHistory() {
|
||||
const chatBox = document.getElementById('chat-history');
|
||||
if (!chatBox) return [];
|
||||
return Array.from(chatBox.querySelectorAll('.msg-user'))
|
||||
.reverse()
|
||||
.map(msg => {
|
||||
const body = msg.querySelector('.body');
|
||||
return msg.dataset?.raw || (body ? body.textContent : '') || '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (messageInput && !messageInput._odysseusPromptRecallCapture) {
|
||||
messageInput._odysseusPromptRecallCapture = true;
|
||||
let recallHistory = [];
|
||||
let recallIndex = -1;
|
||||
let lastRecalled = '';
|
||||
const norm = (v) => String(v || '').replace(/\r\n/g, '\n').trimEnd();
|
||||
messageInput.addEventListener('input', () => {
|
||||
if (norm(messageInput.value) === norm(lastRecalled)) return;
|
||||
recallHistory = [];
|
||||
recallIndex = -1;
|
||||
lastRecalled = '';
|
||||
try { delete messageInput.dataset.odysseusRecallIndex; } catch {}
|
||||
}, true);
|
||||
messageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
|
||||
if (window._ghostAutocomplete?.isActive?.()) return;
|
||||
const fresh = _readComposerPromptHistory();
|
||||
const history = fresh.length ? fresh : recallHistory;
|
||||
if (!history.length) return;
|
||||
const current = norm(messageInput.value);
|
||||
let currentIndex = current ? history.findIndex(item => norm(item) === current) : -1;
|
||||
if (current && currentIndex < 0 && current === norm(lastRecalled)) currentIndex = recallIndex;
|
||||
if (current && currentIndex < 0) {
|
||||
const markedIndex = Number(messageInput.dataset.odysseusRecallIndex);
|
||||
if (Number.isInteger(markedIndex) && markedIndex >= 0 && markedIndex < history.length) {
|
||||
currentIndex = markedIndex;
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (currentIndex < 0) return;
|
||||
const nextIndex = currentIndex - 1;
|
||||
if (nextIndex < 0) {
|
||||
recallHistory = history;
|
||||
recallIndex = -1;
|
||||
lastRecalled = '';
|
||||
try { delete messageInput.dataset.odysseusRecallIndex; } catch {}
|
||||
messageInput.value = '';
|
||||
try { messageInput.selectionStart = messageInput.selectionEnd = 0; } catch {}
|
||||
try { uiModule.autoResize(messageInput); } catch {}
|
||||
return;
|
||||
}
|
||||
const recalled = history[nextIndex];
|
||||
recallHistory = history;
|
||||
recallIndex = nextIndex;
|
||||
lastRecalled = recalled;
|
||||
try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {}
|
||||
messageInput.value = recalled;
|
||||
try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {}
|
||||
try { uiModule.autoResize(messageInput); } catch {}
|
||||
return;
|
||||
}
|
||||
const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0;
|
||||
const recalled = history[nextIndex];
|
||||
if (!recalled) return;
|
||||
recallHistory = history;
|
||||
recallIndex = nextIndex;
|
||||
lastRecalled = recalled;
|
||||
try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {}
|
||||
messageInput.value = recalled;
|
||||
try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {}
|
||||
try { uiModule.autoResize(messageInput); } catch {}
|
||||
}, true);
|
||||
}
|
||||
|
||||
const _sendIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
|
||||
const _micIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
|
||||
const _stopIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
|
||||
@@ -4006,13 +4197,18 @@ function startOdysseusApp() {
|
||||
// Toggle mic/send icon on input change + hide model picker after enough text
|
||||
if (messageInput) {
|
||||
const _debouncedUpdateIcon = uiModule.debounce(_updateSendBtnIcon, 50);
|
||||
const _MODEL_PICKER_HIDE_CHARS = 10;
|
||||
const _syncModelPickerAutohide = () => {
|
||||
const hidePicker = (messageInput.value || '').replace(/\s/g, '').length >= _MODEL_PICKER_HIDE_CHARS;
|
||||
if (modelPickerWrap) {
|
||||
modelPickerWrap.classList.toggle('model-picker-autohide', hidePicker);
|
||||
}
|
||||
};
|
||||
const _MODEL_PICKER_HIDE_CHARS = 23;
|
||||
const _syncModelPickerAutohide = () => {
|
||||
const compactMobile = _isMobileChatInput() && !!(messageInput.value || '').trim();
|
||||
const hidePicker = compactMobile || (messageInput.value || '').replace(/\s/g, '').length >= _MODEL_PICKER_HIDE_CHARS;
|
||||
if (modelPickerWrap) {
|
||||
modelPickerWrap.classList.toggle('model-picker-autohide', hidePicker);
|
||||
}
|
||||
const planStatus = el('plan-mode-status');
|
||||
if (planStatus) {
|
||||
planStatus.classList.toggle('plan-mode-status-autohide', hidePicker);
|
||||
}
|
||||
};
|
||||
window._syncModelPickerAutohide = _syncModelPickerAutohide;
|
||||
_syncModelPickerAutohide();
|
||||
messageInput.addEventListener('input', () => {
|
||||
@@ -4238,23 +4434,119 @@ function startOdysseusApp() {
|
||||
|
||||
// Non-critical startup work must not compete with first paint, chat send, or
|
||||
// chat switching. Panels load their own data when opened; these are only warmups.
|
||||
_syncWelcomeModelHint().catch(() => {});
|
||||
runNonCriticalStartup(() => {
|
||||
modelsModule.refreshModels(false).then(() => {
|
||||
try { sessionModule.updateModelPicker(); } catch (_) {}
|
||||
_syncWelcomeModelHint().catch(() => {});
|
||||
}).catch(() => {});
|
||||
}, 3500);
|
||||
runNonCriticalStartup(() => modelsModule.refreshProviders(), 6500);
|
||||
runNonCriticalStartup(() => ragModule.loadPersonalDocs(), 9000);
|
||||
runNonCriticalStartup(() => memoryModule.loadMemories(), 12000);
|
||||
|
||||
// Ensure proper initial state
|
||||
voiceRecorderModule.init();
|
||||
if (censorModule) censorModule.init();
|
||||
runNonCriticalStartup(() => memoryModule.loadMemories(), 12000);
|
||||
|
||||
// Auto-focus message input on load
|
||||
const msgEl = document.getElementById('message');
|
||||
// Ensure proper initial state
|
||||
voiceRecorderModule.init();
|
||||
if (censorModule) censorModule.init();
|
||||
|
||||
// ── Mobile pull-to-refresh for the active chat ──
|
||||
(function initMobileChatPullRefresh() {
|
||||
const historyEl = document.getElementById('chat-history');
|
||||
const container = document.getElementById('chat-container');
|
||||
if (!historyEl || !container || !('ontouchstart' in window || navigator.maxTouchPoints > 0)) return;
|
||||
|
||||
const THRESHOLD = 72;
|
||||
const MAX_PULL = 104;
|
||||
let startY = 0;
|
||||
let pullY = 0;
|
||||
let tracking = false;
|
||||
let refreshing = false;
|
||||
let spinner = null;
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'chat-pull-refresh';
|
||||
indicator.setAttribute('aria-hidden', 'true');
|
||||
indicator.innerHTML = '<div class="chat-pull-refresh-spinner"></div>';
|
||||
container.prepend(indicator);
|
||||
const spinnerMount = indicator.querySelector('.chat-pull-refresh-spinner');
|
||||
try {
|
||||
spinner = spinnerModule.createWhirlpool(18);
|
||||
spinnerMount.replaceChildren(spinner.element);
|
||||
} catch (_) {}
|
||||
|
||||
function setPull(px, active = false) {
|
||||
pullY = Math.max(0, Math.min(MAX_PULL, px));
|
||||
const pct = Math.min(1, pullY / THRESHOLD);
|
||||
indicator.style.setProperty('--pull-refresh-y', `${pullY}px`);
|
||||
indicator.style.setProperty('--pull-refresh-progress', `${pct}`);
|
||||
indicator.classList.toggle('is-visible', active || refreshing || pullY > 2);
|
||||
indicator.classList.toggle('is-ready', pct >= 1 && !refreshing);
|
||||
indicator.classList.toggle('is-refreshing', refreshing);
|
||||
}
|
||||
|
||||
async function runRefresh() {
|
||||
if (refreshing) return;
|
||||
if (_isForegroundChatBusy()) {
|
||||
setPull(0, false);
|
||||
return;
|
||||
}
|
||||
refreshing = true;
|
||||
setPull(THRESHOLD, true);
|
||||
const safetyTimer = setTimeout(() => {
|
||||
refreshing = false;
|
||||
setPull(0, false);
|
||||
}, 8000);
|
||||
try {
|
||||
const sid = sessionModule && sessionModule.getCurrentSessionId && sessionModule.getCurrentSessionId();
|
||||
if (sid && sessionModule.selectSession) {
|
||||
await sessionModule.selectSession(sid, { keepSidebar: true, showLoading: false, immediateLoading: true });
|
||||
} else if (sessionModule && sessionModule.loadSessions) {
|
||||
await sessionModule.loadSessions();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('pull refresh failed:', err);
|
||||
} finally {
|
||||
clearTimeout(safetyTimer);
|
||||
refreshing = false;
|
||||
setPull(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
historyEl.addEventListener('touchstart', (e) => {
|
||||
if (refreshing || window.innerWidth > 768) return;
|
||||
if (document.querySelector('.modal:not(.hidden)')) return;
|
||||
if (historyEl.scrollTop > 0) return;
|
||||
if (e.target && e.target.closest && e.target.closest('.chat-input-bar, textarea, input, button, select, a')) return;
|
||||
tracking = true;
|
||||
startY = e.touches[0].clientY;
|
||||
setPull(0, false);
|
||||
}, { passive: true });
|
||||
|
||||
historyEl.addEventListener('touchmove', (e) => {
|
||||
if (!tracking || refreshing) return;
|
||||
const dy = e.touches[0].clientY - startY;
|
||||
if (dy <= 0) {
|
||||
setPull(0, false);
|
||||
return;
|
||||
}
|
||||
if (historyEl.scrollTop <= 0) {
|
||||
e.preventDefault();
|
||||
setPull(dy * 0.62, true);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
historyEl.addEventListener('touchend', () => {
|
||||
if (!tracking) return;
|
||||
tracking = false;
|
||||
if (pullY >= THRESHOLD) runRefresh();
|
||||
else setPull(0, false);
|
||||
}, { passive: true });
|
||||
|
||||
historyEl.addEventListener('touchcancel', () => {
|
||||
tracking = false;
|
||||
if (!refreshing) setPull(0, false);
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
// Auto-focus message input on load
|
||||
const msgEl = document.getElementById('message');
|
||||
if (msgEl) msgEl.focus();
|
||||
|
||||
// Initialize mouse-based drag for sidebar sections
|
||||
|
||||
Reference in New Issue
Block a user