Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent c9dd68d890
commit 84aa9a91de
871 changed files with 265870 additions and 27854 deletions
+214 -124
View File
@@ -3,16 +3,16 @@
// ES6 module — entry point, no exports (wires all modules together)
// ============================================
import Storage from './js/storage.js';
import uiModule from './js/ui.js';
import uiModule from './js/ui.js?v=20260908weekhoverfix1';
import workspaceModule from './js/workspace.js';
import fileHandlerModule from './js/fileHandler.js';
import modelsModule from './js/models.js?v=20260715startupcalm2';
import fileHandlerModule from './js/fileHandler.js?v=20260909mobileattachmentedit1';
import modelsModule from './js/models.js';
import ragModule from './js/rag.js';
import presetsModule from './js/presets.js';
import presetsModule from './js/presets.js?v=20260908personaname1';
import searchModule from './js/search.js';
import chatModule from './js/chat.js?v=20260819approvalcontrol1';
import compareModule from './js/compare/index.js?v=20260819approvalcontrol1';
import documentModule from './js/document.js?v=20260815approvalsave1';
import chatModule from './js/chat.js?v=20260910shelltoggle1';
import compareModule from './js/compare/index.js?v=20260909mobilepaneaddscroll1';
import documentModule from './js/document.js?v=20260910minimizedcontext1';
import searchChatModule from './js/search-chat.js';
import { makeWindowDraggable } from './js/windowDrag.js';
import {
@@ -22,23 +22,30 @@ import {
settleSessionHydration
} from './js/startupShell.js';
import markdownModule from './js/markdown.js';
import chatRenderer from './js/chatRenderer.js?v=20260819approvalcontrol1';
import chatRenderer from './js/chatRenderer.js?v=20260910streamlinks2';
// Keep this specifier identical to every consumer (especially chat.js).
// Different query strings create separate ES-module instances with separate
// current-session state, so the picker can display one model while chat sends
// through another session.
import sessionModule from './js/sessions.js';
import memoryModule from './js/memory.js?v=20260722memoryloading1';
import { startBackgroundToolJobs } from './js/backgroundToolJobs.js';
import memoryModule from './js/memory.js';
import voiceRecorderModule from './js/voiceRecorder.js';
import censorModule from './js/censor.js';
import galleryModule from './js/gallery.js';
import { UI_VIS_DEFAULT_OFF, resolveVisibility } from './js/ui_visibility.js';
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?v=20260716openrouter3';
import settingsModule from './js/settings.js?v=20260815approvalsave1';
import galleryModule from './js/gallery.js?v=20260910promptcopy1';
import { UI_VIS_DEFAULT_OFF, resolveVisibility } from './js/ui_visibility.js?v=20260829chatstyle12';
import tasksModule from './js/tasks.js?v=20260910tasksortpicker6';
import calendarModule from './js/calendar.js?v=20260903weekscrollstable1';
import notesModule from './js/notes.js?v=20260911notesselectioncancel1';
import adminModule from './js/admin.js?v=20260908notificationcopy1';
import settingsModule from './js/settings.js?v=20260909defaultmodelfix1';
// Eagerly bind unified minimize/restore behavior across all tool modals.
import './js/modalManager.js?v=20260723compareicon2';
import './js/modalManager.js';
import './js/chipScroll.js?v=20260903calendarchips1';
import './js/mobileBulkSelect.js?v=20260910selecthold1';
// Desktop window tiling — drag a modal near an edge/corner to snap.
import './js/tileManager.js';
import themeModule from './js/theme.js';
import './js/tileManager.js?v=20260910responsivebounds1';
import themeModule from './js/theme.js?v=20260909effectspeed1';
// IMPORTANT: import cookbook.js with NO ?v= query — the same plain specifier
// every other importer (cookbook-hwfit.js / cookbook-diagnosis.js) uses. A query
// mismatch makes the browser load cookbook.js twice as separate modules (two
@@ -46,12 +53,12 @@ import themeModule from './js/theme.js';
// unversioned so this can't recur.
import cookbookModule from './js/cookbook.js';
import groupModule from './js/group.js';
import * as researchPanelModule from './js/research/panel.js?v=20260630researchthumb';
import * as researchPanelModule from './js/research/panel.js?v=20260910researchdeeplink1';
import ttsModule from './js/tts-ai.js';
import spinnerModule from './js/spinner.js';
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js?v=20260829chatstyle12';
import { getSettings } from './js/appConfig.js';
import { initSidebarLayout, syncRailSide } from './js/sidebar-layout.js?v=20260715startupclean';
import { initSidebarLayout, syncRailSide } from './js/sidebar-layout.js?v=20260910sidebarbounce1';
import { initSectionCollapse, initSectionDrag } from './js/section-management.js';
const API_BASE = window.location.origin;
@@ -176,8 +183,9 @@ function initRailHoverLabels() {
'rail-email': 'Email',
'rail-gallery': 'Gallery',
'rail-archive': 'Library',
'rail-memory': 'Brain',
'rail-memory': 'Memory',
'rail-notes': 'Notes',
'rail-skills': 'Skills',
'rail-tasks': 'Tasks',
'rail-theme': 'Theme',
'rail-settings': 'Settings',
@@ -237,12 +245,12 @@ async function _refreshDefaultChat() {
// synchronously; later reads should call _refreshDefaultChat() first.
_refreshDefaultChat();
async function _createDirectChatFromPreferredModel() {
async function _createDirectChatFromPreferredModel(opts = {}) {
if (!sessionModule) return false;
const pending = sessionModule.getPendingChat && sessionModule.getPendingChat();
if (pending && pending.url && pending.modelId && pending.endpointId) {
sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId, { source: pending.source || 'manual' });
sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId, { source: pending.source || 'manual', keepDocument: !!opts.keepDocument });
return true;
}
@@ -250,20 +258,20 @@ async function _createDirectChatFromPreferredModel() {
const currentId = sessionModule.getCurrentSessionId();
const current = sessions.find(s => s.id === currentId);
if (current && current.endpoint_url && current.model && current.endpoint_id) {
sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id);
sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id, { keepDocument: !!opts.keepDocument });
return true;
}
const dc = await _refreshDefaultChat();
if (dc) {
sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default' });
sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default', keepDocument: !!opts.keepDocument });
return true;
}
const withModel = sessions.filter(s => s.endpoint_url && s.model);
if (withModel.length > 0) {
const last = withModel[0]; // sessions are sorted by recent
sessionModule.createDirectChat(last.endpoint_url, last.model, last.endpoint_id);
sessionModule.createDirectChat(last.endpoint_url, last.model, last.endpoint_id, { keepDocument: !!opts.keepDocument });
return true;
}
@@ -355,7 +363,8 @@ function initializeEventListeners() {
const link = e.target.closest('a.chat-link');
if (!link) return;
const href = link.getAttribute('href');
if (href && href.startsWith('#') && sessionModule) {
const isListExpander = /^#(?:notes|skills|memories|events|sessions)-more-/.test(href || '');
if (href && href.startsWith('#') && !isListExpander && sessionModule) {
e.preventDefault();
sessionModule.selectSession(href.slice(1));
}
@@ -392,19 +401,54 @@ function initializeEventListeners() {
const exportMenu = el('export-dropdown-menu');
if (exportDlBtn && exportMenu) {
exportDlBtn.addEventListener('click', (e) => {
e.stopPropagation();
// Keep Settings available from the chat menu even before a new chat has
// received its first prompt (when the header has no session actions yet).
if (!el('export-settings-btn')) {
const settingsItem = document.createElement('div');
settingsItem.className = 'export-dropdown-item';
settingsItem.id = 'export-settings-btn';
settingsItem.classList.add('export-settings-text-only');
settingsItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 7.04 4.3l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09A1.65 1.65 0 0 0 15 4.6a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09A1.65 1.65 0 0 0 19.4 15z"></path></svg></span><span>Settings</span>';
const deleteItem = exportMenu.querySelector('#export-delete-btn');
if (deleteItem) exportMenu.insertBefore(settingsItem, deleteItem);
else exportMenu.appendChild(settingsItem);
settingsItem.addEventListener('click', (e) => {
e.stopPropagation();
exportMenu.classList.remove('open');
if (window.chatModule?.openContextSettings && window.chatModule.openContextSettings()) return;
if (typeof settingsModule !== 'undefined' && settingsModule?.open) settingsModule.open();
else if (typeof adminModule !== 'undefined' && adminModule?.open) adminModule.open();
else if (window.settingsModule?.open) window.settingsModule.open();
else if (window.adminModule?.open) window.adminModule.open();
});
}
const openExportMenuFrom = (anchorEl, { align = 'right' } = {}) => {
if (exportMenu.classList.contains('open')) {
exportMenu.classList.remove('open');
} else {
// Move menu to body so it's not affected by ancestor transforms
if (exportMenu.parentElement !== document.body) document.body.appendChild(exportMenu);
const rect = exportDlBtn.getBoundingClientRect();
const rect = (anchorEl || exportDlBtn).getBoundingClientRect();
exportMenu.style.top = (rect.bottom + 4) + 'px';
exportMenu.style.left = 'auto';
exportMenu.style.right = (window.innerWidth - rect.right) + 'px';
if (align === 'left') {
exportMenu.style.left = Math.max(8, rect.left) + 'px';
exportMenu.style.right = 'auto';
} else {
exportMenu.style.left = 'auto';
exportMenu.style.right = (window.innerWidth - rect.right) + 'px';
}
exportMenu.classList.add('open');
if (align === 'left') {
const menuRect = exportMenu.getBoundingClientRect();
if (menuRect.right > window.innerWidth - 8) {
exportMenu.style.left = `${Math.max(8, window.innerWidth - menuRect.width - 8)}px`;
}
}
}
};
exportDlBtn.addEventListener('click', (e) => {
e.stopPropagation();
openExportMenuFrom(exportDlBtn);
});
document.addEventListener('click', () => exportMenu.classList.remove('open'));
document.addEventListener('keydown', (e) => {
@@ -429,8 +473,17 @@ function initializeEventListeners() {
if (currentMeta) {
currentMeta.style.cursor = 'pointer';
currentMeta.addEventListener('click', (e) => {
const incognitoChk = el('incognito-toggle');
const mobile = window.matchMedia && window.matchMedia('(max-width: 768px)').matches;
if (mobile && incognitoChk && incognitoChk.checked) {
e.preventDefault();
e.stopPropagation();
const incognitoBtnForTitle = el('incognito-btn');
if (incognitoBtnForTitle) incognitoBtnForTitle.click();
return;
}
e.stopPropagation();
exportDlBtn.click();
openExportMenuFrom(currentMeta, { align: 'left' });
});
}
}
@@ -496,20 +549,6 @@ 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) {
@@ -535,33 +574,6 @@ function initializeEventListeners() {
});
}
// Export: Save to Docs
const exportDocBtn = el('export-doc-btn');
if (exportDocBtn) {
exportDocBtn.addEventListener('click', async (e) => {
e.stopPropagation();
exportMenu.classList.remove('open');
try {
const sessionId = sessionModule.getCurrentSessionId();
const texts = _serializeChatTranscript();
const meta = sessionModule.getSessions().find(s => s.id === sessionId);
const title = meta?.name || 'Untitled';
const res = await fetch(`${API_BASE}/api/document`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, title, content: texts }),
});
if (!res.ok) throw new Error('Failed');
const doc = await res.json();
if (documentModule) documentModule.loadDocument(doc.id);
uiModule.showToast('Saved to documents');
} catch (err) {
console.error('Save to docs failed:', err);
uiModule.showError('Failed to save to documents');
}
});
}
// Export menu: Delete current chat
const exportDeleteBtn = el('export-delete-btn');
if (exportDeleteBtn) {
@@ -1094,6 +1106,14 @@ function initializeEventListeners() {
}
});
}
const notesNewNoteBtn = el('notes-new-note-btn');
if (notesNewNoteBtn) {
notesNewNoteBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
notesModule?.newNote?.();
});
}
// Refresh notes due-reminder badge on load and every 5 minutes
if (notesModule && notesModule.refreshDueBadge) {
notesModule.refreshDueBadge();
@@ -1220,6 +1240,7 @@ function initializeEventListeners() {
setTimeout(_goFullscreen, 200);
},
'/memory': () => document.getElementById('tool-memory-btn')?.click(),
'/skills': () => document.getElementById('tool-skills-btn')?.click(),
'/gallery': () => document.getElementById('tool-gallery-btn')?.click(),
'/tasks': () => document.getElementById('tool-tasks-btn')?.click(),
'/library': () => sessionModule && sessionModule.openLibrary && sessionModule.openLibrary(),
@@ -1675,9 +1696,24 @@ function initializeEventListeners() {
const toolMemoryBtn = el('tool-memory-btn');
if (toolMemoryBtn && memoryModal) {
toolMemoryBtn.addEventListener('click', () => {
memoryModal.classList.remove('hidden');
if (memoryModule && memoryModule.renderMemoryList) memoryModule.renderMemoryList();
if (memoryModule && memoryModule.updateMemoryCount) memoryModule.updateMemoryCount();
if (memoryModule && memoryModule.openMemoryModal) memoryModule.openMemoryModal('browse');
else {
memoryModal.classList.remove('hidden');
if (memoryModule && memoryModule.renderMemoryList) memoryModule.renderMemoryList();
if (memoryModule && memoryModule.updateMemoryCount) memoryModule.updateMemoryCount();
}
});
}
// Sidebar Skills button
const toolSkillsBtn = el('tool-skills-btn');
if (toolSkillsBtn && memoryModal) {
toolSkillsBtn.addEventListener('click', () => {
if (memoryModule && memoryModule.openMemoryModal) memoryModule.openMemoryModal('skills');
else {
memoryModal.classList.remove('hidden');
document.querySelector('.memory-tab[data-memory-tab="skills"]')?.click();
}
});
}
@@ -1836,8 +1872,10 @@ function initializeEventListeners() {
if (toggle) toggle.classList.toggle('mode-chat', mode === 'chat');
// Workspace pill + overflow entry are agent-only - hide immediately (no flash).
try { workspaceModule.applyMode(mode); } catch (_) {}
// Delay tool glow-up for a staggered effect
setTimeout(() => applyModeToToggles(mode), 500);
// Tool state is request authority, so synchronize it immediately. A
// delayed callback could overwrite a user's fast toggle click and send
// a different allow_bash/allow_web_search value than the UI showed.
applyModeToToggles(mode);
}
window.__odysseusSetChatMode = setMode;
agentBtn.addEventListener('click', () => {
@@ -2178,13 +2216,12 @@ function initializeEventListeners() {
menu.classList.add('closing');
plusBtn.classList.remove('expanded');
if (pickerWrap) pickerWrap.style.visibility = '';
// Item delays max at 0.18s + 0.20s anim = 0.38s for items, container
// delay 0.16s + 0.22s = 0.38s. 400ms covers both with margin.
// Keep closing nearly immediate so the tools button feels direct.
setTimeout(() => {
menu.classList.add('hidden');
menu.classList.remove('closing');
if (ownerWrap) ownerWrap.appendChild(menu); // restore from <body> portal
}, 400);
}, 100);
}
// Close menu when clicking any item inside it. preventDefault on pointerdown
// so tapping an item (e.g. Attach files) doesn't steal focus from the message
@@ -2568,9 +2605,15 @@ function initializeEventListeners() {
// ── Group toggle button (chatbox indicator) — click to deactivate ──
const groupToggleBtn = el('group-toggle-btn');
if (groupToggleBtn) {
groupToggleBtn.addEventListener('click', () => {
_syncGroupIndicator(false);
groupModule.stopGroup();
groupToggleBtn.addEventListener('click', (e) => {
if (e.target.closest('.tool-indicator-x')) {
_syncGroupIndicator(false);
groupModule.stopGroup();
return;
}
if (presetsModule && presetsModule.openCustomPresetModal) {
presetsModule.openCustomPresetModal('group');
}
});
}
@@ -2592,14 +2635,16 @@ function initializeEventListeners() {
if (incognitoBtn) {
incognitoBtn.addEventListener('mousedown', (e) => e.preventDefault());
incognitoBtn.addEventListener('click', () => {
// Don't toggle mid-chat — incognito only changeable from welcome screen
const ws = el('welcome-screen');
if (ws && ws.classList.contains('hidden')) return;
const chk = el('incognito-toggle');
// Starting Nobody is only available from the welcome screen, but ending
// it must work after the first message too. The active header/title
// controls depend on this path once the welcome button is hidden.
if (ws && ws.classList.contains('hidden') && !chk.checked) return;
chk.checked = !chk.checked;
incognitoBtn.classList.toggle('active', chk.checked);
const tipEl = el('welcome-tip');
incognitoBtn.title = chk.checked ? 'Disable Nobody mode' : 'Enable Nobody mode — no memory, no history saved';
incognitoBtn.title = chk.checked ? 'Disable Nobody mode' : 'Enable Nobody mode — no memory or skill injection, no history saved';
const welcomeName = document.querySelector('.welcome-name');
if (chk.checked) {
try {
@@ -2623,7 +2668,13 @@ function initializeEventListeners() {
welcomeSub.textContent = "Who am I? I'm nobody.";
welcomeSub.style.display = '';
}
if (tipEl) { tipEl.dataset.originalTip = tipEl.textContent; tipEl.textContent = 'Temporary session \u2014 won\u2019t be saved and no memory activation.'; tipEl.style.opacity = '0.5'; tipEl.style.marginTop = '8px'; }
if (tipEl) {
tipEl.dataset.originalTip = tipEl.textContent;
tipEl.classList.add('nobody-session-note');
tipEl.innerHTML = '<span>No memory or skill activation.</span><span>Temporary session \u2014 won\u2019t be saved.</span>';
tipEl.style.opacity = '0.5';
tipEl.style.marginTop = '16px';
}
// Default to plain chat: disable tools visually, switch to chat mode.
// IMPORTANT: don't overwrite the user's persisted per-mode tool prefs
// (`web_agent`, `bash_agent`, `web_chat`, `bash_chat`). Nobody mode is
@@ -2673,7 +2724,13 @@ function initializeEventListeners() {
}
welcomeSub2.style.display = '';
}
if (tipEl && tipEl.dataset.originalTip) { tipEl.textContent = tipEl.dataset.originalTip; tipEl.style.opacity = ''; tipEl.style.marginTop = ''; }
if (tipEl && Object.prototype.hasOwnProperty.call(tipEl.dataset, 'originalTip')) {
tipEl.textContent = tipEl.dataset.originalTip;
delete tipEl.dataset.originalTip;
tipEl.classList.remove('nobody-session-note');
tipEl.style.opacity = '';
tipEl.style.marginTop = '';
}
// Heal any previously-persisted false values from the old Nobody bug
// so agent-mode defaults (web/bash ON) come back.
const _ts = Storage.getJSON(Storage.KEYS.TOGGLES, {});
@@ -3145,6 +3202,7 @@ function initializeEventListeners() {
function injectMinimizeButton(modal) {
if (!modal || !modal.classList || !modal.classList.contains('modal')) return;
if (modal.dataset.noMinimize === 'true') return;
if (modal.id && SKIP_IDS.has(modal.id)) return;
// Modals managed by the new modalManager (Modals.register) get their own
// .modal-minimize-btn and chips via the .minimized-dock-chip system.
@@ -3313,13 +3371,40 @@ function initializeEventListeners() {
async function _handleNewChatAction({ preferModel = true, focus = true } = {}) {
if (!sessionModule) return;
if (_closeCompareIfActive()) return;
const hasOpenDocument = !!(
documentModule
&& documentModule.isPanelOpen
&& documentModule.isPanelOpen()
&& documentModule.getCurrentDocId
&& documentModule.getCurrentDocId()
);
const shouldCarryDocument = hasOpenDocument && !!(
uiModule?.styledConfirm
? await uiModule.styledConfirm('Bring open document to new chat?', {
title: 'New chat',
confirmText: 'OK',
cancelText: 'No',
})
: window.confirm('Bring open document to new chat?')
);
_deactivateIncognito();
// Clear character on new chat
if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
// Clear research mode if active
const _resChk = el('research-toggle');
if (_resChk && _resChk.checked) _syncResearchIndicator(false);
if (preferModel && await _createDirectChatFromPreferredModel()) return;
if (preferModel && await _createDirectChatFromPreferredModel({ keepDocument: shouldCarryDocument })) {
if (shouldCarryDocument) {
try {
await sessionModule.materializePendingSession();
await documentModule.moveActiveDocumentToCurrentChat?.({ quiet: true });
} catch (e) {
console.warn('Failed to carry document into new chat:', e);
uiModule?.showError?.('Could not bring document into the new chat');
}
}
return;
}
// No models at all — show welcome screen
_startFreshChat();
const docBtn3 = el('overflow-doc-btn');
@@ -3346,17 +3431,8 @@ function initializeEventListeners() {
// the default model attaches when they hit send.
const mobileNewChat = el('mobile-new-chat-btn');
if (mobileNewChat) {
mobileNewChat.addEventListener('click', () => {
if (!sessionModule) return;
if (_closeCompareIfActive()) return;
_deactivateIncognito();
_startFreshChat();
document.querySelectorAll('.session-item.active').forEach(s => s.classList.remove('active'));
// Focus the composer synchronously so mobile keyboards pop open.
// iOS Safari only honours programmatic focus inside the original click
// callback — a setTimeout breaks the user-gesture chain.
const _input = el('message-input');
if (_input) { try { _input.focus(); } catch (_) {} }
mobileNewChat.addEventListener('click', async () => {
await _handleNewChatAction();
});
}
@@ -3707,6 +3783,8 @@ function startOdysseusApp() {
searchModule.init();
chatModule.init(API_BASE);
chatModule.initListeners();
startBackgroundToolJobs({ getSessionId: () => sessionModule.getCurrentSessionId(),
addMessage: (...args) => chatRenderer.addMessage(...args), base: API_BASE });
groupModule.init(API_BASE);
// Initialize compare module
if (compareModule) {
@@ -3737,15 +3815,16 @@ function startOdysseusApp() {
// Rail tool buttons — delegate to sidebar tool buttons
const _railToolMap = {
'rail-compare': 'tool-compare-btn',
'rail-research': 'tool-research-btn',
'rail-cookbook': 'tool-cookbook-btn',
'rail-archive': 'tool-library-btn',
'rail-gallery': 'tool-gallery-btn',
'rail-tasks': 'tool-tasks-btn',
'rail-calendar': 'tool-calendar-btn',
'rail-notes': 'tool-notes-btn',
'rail-compare': 'tool-compare-btn',
'rail-cookbook': 'tool-cookbook-btn',
'rail-research': 'tool-research-btn',
'rail-gallery': 'tool-gallery-btn',
'rail-archive': 'tool-library-btn',
'rail-memory': 'tool-memory-btn',
'rail-notes': 'tool-notes-btn',
'rail-skills': 'tool-skills-btn',
'rail-tasks': 'tool-tasks-btn',
'rail-theme': 'tool-theme-btn',
'rail-email': 'email-section-title',
};
@@ -4030,12 +4109,18 @@ function startOdysseusApp() {
return;
}
// New chat mode — empty input, no attachments, no STT
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
if (sessionModule) {
const sessions = sessionModule.getSessions();
const currentId = sessionModule.getCurrentSessionId();
const current = sessions.find(s => s.id === currentId);
// New chat mode — empty input, no attachments, no STT
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
const isAlreadyNewChat = document.getElementById('chat-container')?.classList.contains('welcome-active');
if (isAlreadyNewChat) {
if (messageInput) messageInput.focus();
_updateSendBtnIcon();
return;
}
if (sessionModule) {
const sessions = sessionModule.getSessions();
const currentId = sessionModule.getCurrentSessionId();
const current = sessions.find(s => s.id === currentId);
if (current && current.endpoint_url && current.model) {
sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id);
} else {
@@ -4076,13 +4161,18 @@ function startOdysseusApp() {
e.preventDefault();
// Flush the debounced icon update so dataset.mode reflects the current
// text state. Without this, a fast type-and-Enter would still see the
// stale 'newchat' mode and open a new chat instead of sending.
try { _updateSendBtnIcon(); } catch {}
if (sendBtn && sendBtn.dataset.mode === 'newchat') {
const railNew = el('rail-new-session');
if (railNew) railNew.click();
return;
}
// stale 'newchat' mode and open a new chat instead of sending.
try { _updateSendBtnIcon(); } catch {}
if (sendBtn && sendBtn.dataset.mode === 'newchat') {
const isAlreadyNewChat = document.getElementById('chat-container')?.classList.contains('welcome-active');
if (isAlreadyNewChat) {
if (messageInput) messageInput.focus();
return;
}
const railNew = el('rail-new-session');
if (railNew) railNew.click();
return;
}
if (_isForegroundChatBusy() && messageInput.value && messageInput.value.trim()) {
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
return;