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
+208 -35
View File
@@ -4,10 +4,10 @@
* UI utilities for toasts, modals, scrolling, and user feedback
*/
import themeModule from './theme.js';
import themeModule from './theme.js?v=20260909effectspeed1';
import * as Modals from './modalManager.js';
import spinnerModule from './spinner.js';
import { registerMenuDismiss, dismissTopMenu, dismissOrRemove } from './escMenuStack.js';
import { registerMenuDismiss, dismissTopEscapeLayer, dismissOrRemove } from './escMenuStack.js';
import { nextToolWindowZ, topToolWindowZ } from './toolWindowZOrder.js';
let toastEl = null;
@@ -31,6 +31,38 @@ function _targetEl(target) {
return target && target.nodeType === 1 ? target : target?.parentElement || null;
}
function _positionToastForOpenPanel(el) {
if (!el) return;
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
const panel = [...document.querySelectorAll('.notes-pane')]
.filter((candidate) => {
if (!document.contains(candidate)) return false;
const style = getComputedStyle(candidate);
if (style.display === 'none' || style.visibility === 'hidden') return false;
const rect = candidate.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && rect.right > 0 && rect.left < vw;
})
.sort((a, b) => b.getBoundingClientRect().left - a.getBoundingClientRect().left)[0];
el.classList.remove('toast-panel-offset');
el.style.left = '';
el.style.right = '';
el.style.maxWidth = '';
if (!panel) return;
const rect = panel.getBoundingClientRect();
// There needs to be room for a toast on the panel's left. On narrow/mobile
// layouts the panel can occupy the whole width, so retain the normal edge
// position instead of forcing the toast underneath the panel.
const gap = 8;
const availableWidth = rect.left - (gap * 2);
if (availableWidth < 220) return;
el.style.left = 'auto';
el.style.right = `${Math.max(12, vw - rect.left + gap)}px`;
el.style.maxWidth = `${Math.min(420, availableWidth)}px`;
el.classList.add('toast-panel-offset');
}
const SPACE_CARD_SELECTOR = [
'#email-lib-modal .doclib-card',
'#doclib-modal .doclib-card',
@@ -281,6 +313,7 @@ function _wireToastSwipe(el) {
setTimeout(() => {
el.classList.remove('show');
el.classList.add('exiting');
el.style.pointerEvents = '';
el.style.transform = '';
el.style.opacity = '';
}, 180);
@@ -298,14 +331,20 @@ function _wireToastSwipe(el) {
* Show success toast message
*/
export function showToast(msg, durationOrOpts) {
fetch('/api/tasks/notification-logs', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: String(msg || ''), status: 'success' }),
}).catch(() => {});
if (!toastEl) {
toastEl = document.getElementById('toast');
}
_wireToastSwipe(toastEl);
toastEl.textContent = '';
toastEl.classList.remove('error');
toastEl.classList.remove('toast-ai-reply-progress', 'toast-ai-reply-result', 'toast-sending', 'toast-message-sent');
let duration = 1200, actionLabel = null, onAction = null, actionHint = null, actionIcon = null, leadingIcon = null;
let duration = 1200, actionLabel = null, onAction = null, actionHint = null, actionIcon = null, leadingIcon = null, toastClass = null, aiReplyProgress = false, aiReplyResult = false;
if (typeof durationOrOpts === 'object' && durationOrOpts) {
duration = durationOrOpts.duration || 5000;
actionLabel = durationOrOpts.action;
@@ -313,11 +352,18 @@ export function showToast(msg, durationOrOpts) {
actionHint = durationOrOpts.actionHint || null;
actionIcon = durationOrOpts.actionIcon || null;
leadingIcon = durationOrOpts.leadingIcon || null;
toastClass = durationOrOpts.toastClass || null;
aiReplyProgress = !!durationOrOpts.aiReplyProgress;
aiReplyResult = !!durationOrOpts.aiReplyResult;
} else if (typeof durationOrOpts === 'number') {
duration = durationOrOpts;
}
if (aiReplyProgress) toastEl.classList.add('toast-ai-reply-progress');
if (aiReplyResult) toastEl.classList.add('toast-ai-reply-result');
if (toastClass) toastEl.classList.add(toastClass);
const textSpan = document.createElement('span');
textSpan.className = 'toast-message';
if (leadingIcon === 'check') {
const icon = document.createElement('span');
icon.className = 'toast-checkmark';
@@ -333,36 +379,40 @@ export function showToast(msg, durationOrOpts) {
textSpan.textContent = msg;
toastEl.appendChild(textSpan);
if (actionLabel && onAction) {
// Wrap the action in a small column so we can stack a Ctrl-Z-style hint
// directly under the button.
const stack = document.createElement('span');
stack.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:1px;margin-left:10px;line-height:1;';
const actionGroup = document.createElement('span');
actionGroup.className = 'toast-actions';
if (actionLabel && onAction) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'toast-action-btn';
if (actionIcon) {
btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:5px;">${actionIcon}<span></span></span>`;
btn.querySelector('span span').textContent = actionLabel;
const icon = document.createElement('span');
icon.className = 'toast-action-icon';
icon.innerHTML = actionIcon;
btn.appendChild(icon);
const label = document.createElement('span');
label.textContent = actionLabel;
btn.appendChild(label);
} else {
btn.textContent = actionLabel;
const label = document.createElement('span');
label.textContent = actionLabel;
btn.appendChild(label);
}
if (actionHint && window.innerWidth > 768) {
const hint = document.createElement('kbd');
hint.className = 'toast-action-hint';
hint.textContent = actionHint;
btn.appendChild(hint);
}
btn.style.cssText = 'padding:2px 10px;border:1px solid var(--fg);border-radius:4px;background:none;color:var(--fg);cursor:pointer;font-size:12px;pointer-events:auto;display:inline-flex;align-items:center;';
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
toastEl.classList.remove('show');
toastEl.style.pointerEvents = '';
onAction();
});
stack.appendChild(btn);
if (actionHint && window.innerWidth > 768) {
const hint = document.createElement('span');
hint.textContent = actionHint;
hint.style.cssText = 'font-size:9px;opacity:0.55;letter-spacing:0.4px;text-transform:uppercase;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin-top:1px;pointer-events:none;';
stack.appendChild(hint);
}
toastEl.appendChild(stack);
actionGroup.appendChild(btn);
toastEl.style.pointerEvents = 'auto';
} else {
toastEl.style.pointerEvents = '';
@@ -383,13 +433,14 @@ export function showToast(msg, durationOrOpts) {
toastEl.classList.remove('show');
toastEl.style.pointerEvents = '';
});
toastEl.appendChild(closeBtn);
actionGroup.appendChild(closeBtn);
toastEl.appendChild(actionGroup);
// Pin to top-right via CSS — clear any legacy inline overrides so the
// slide-in-from-right / slide-out-to-left transition can run cleanly.
toastEl.style.left = '';
toastEl.style.transform = '';
toastEl.classList.remove('exiting');
_positionToastForOpenPanel(toastEl);
toastEl.classList.add('show');
clearTimeout(toastEl._hideTimer);
toastEl._hideTimer = setTimeout(() => {
@@ -411,22 +462,37 @@ export function showToast(msg, durationOrOpts) {
* Show error toast message
*/
export function showError(msg) {
fetch('/api/tasks/notification-logs', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: String(msg || ''), status: 'error' }),
}).catch(() => {});
if (!toastEl) {
toastEl = document.getElementById('toast');
}
_wireToastSwipe(toastEl);
toastEl.textContent = '';
toastEl.classList.add('error');
toastEl.style.left = '';
toastEl.style.pointerEvents = '';
toastEl.style.transform = '';
toastEl.classList.remove('exiting');
_positionToastForOpenPanel(toastEl);
toastEl.classList.add('show');
clearTimeout(toastEl._hideTimer);
const icon = document.createElement('span');
icon.className = 'toast-error-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = '!';
toastEl.appendChild(icon);
const textSpan = document.createElement('span');
textSpan.className = 'toast-message';
textSpan.textContent = msg;
toastEl.appendChild(textSpan);
const actionGroup = document.createElement('span');
actionGroup.className = 'toast-actions';
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'toast-close-btn';
@@ -441,11 +507,13 @@ export function showError(msg) {
toastEl.classList.remove('show');
toastEl.style.pointerEvents = '';
});
toastEl.appendChild(closeBtn);
actionGroup.appendChild(closeBtn);
toastEl.appendChild(actionGroup);
toastEl._hideTimer = setTimeout(() => {
toastEl.classList.add('exiting');
toastEl.classList.remove('show');
toastEl.style.pointerEvents = '';
}, 6000);
}
@@ -456,7 +524,7 @@ export function showError(msg) {
let _scrollThrottleTimer = null;
export function scrollHistory() {
if (!autoScrollEnabled) return;
if (!_scrollBox) {
if (!_scrollBox || !_scrollBox.isConnected) {
_scrollBox = document.getElementById('chat-history');
}
// Throttle: only start a new scroll animation every 500ms
@@ -467,6 +535,51 @@ export function scrollHistory() {
}
}
/**
* Capture and restore chat scroll around a synchronous terminal render.
*
* The streamed and persisted versions of a response can have different
* heights, especially in the narrow chat column beside an open document.
* Cancel the old smooth-scroll target and keep the viewport anchored while
* that DOM is replaced so completion cannot produce a rubber-band jump.
*/
export function captureHistoryScroll() {
const box = document.getElementById('chat-history');
if (!box) return null;
const maxScrollTop = Math.max(0, box.scrollHeight - box.clientHeight);
return {
box,
scrollTop: box.scrollTop,
stickToBottom: maxScrollTop - box.scrollTop <= 120,
};
}
export function restoreHistoryScroll(snapshot) {
const box = snapshot?.box;
if (!box || !box.isConnected) return;
if (_scrollRafId) {
cancelAnimationFrame(_scrollRafId);
_scrollRafId = null;
}
if (_scrollThrottleTimer) {
clearTimeout(_scrollThrottleTimer);
_scrollThrottleTimer = null;
}
_scrollBox = box;
const apply = () => {
if (!box.isConnected) return;
box.scrollTop = snapshot.stickToBottom
? Math.max(0, box.scrollHeight - box.clientHeight)
: snapshot.scrollTop;
};
apply();
// Footer metrics and highlighted code settle on the following frames.
requestAnimationFrame(() => {
apply();
requestAnimationFrame(apply);
});
}
function _smoothScrollStep() {
const box = _scrollBox;
if (!box || !autoScrollEnabled) {
@@ -615,8 +728,13 @@ export function styledConfirm(message, { confirmText = 'Confirm', cancelText = '
if (titleEl) titleEl.textContent = title || 'Confirm';
msgEl.textContent = message;
okBtn.textContent = confirmText;
cancelBtn.textContent = cancelText;
// Use the affirmative label consistently for generic confirmations.
okBtn.textContent = confirmText === 'OK' ? 'Yes' : confirmText;
cancelBtn.textContent = '';
const cancelLabel = document.createElement('span');
cancelLabel.className = 'styled-confirm-cancel-label';
cancelLabel.textContent = cancelText;
cancelBtn.appendChild(cancelLabel);
altBtn.textContent = alternateText || '';
okBtn.className = danger ? 'confirm-btn confirm-btn-danger' : 'confirm-btn confirm-btn-primary';
cancelBtn.className = 'confirm-btn confirm-btn-secondary';
@@ -863,6 +981,8 @@ const uiModule = {
styledPrompt,
scrollHistory,
scrollHistoryInstant,
captureHistoryScroll,
restoreHistoryScroll,
setAutoScroll,
getAutoScroll,
autoResize,
@@ -1261,22 +1381,68 @@ if (!window._odyEscExpandGuard) {
// (the live-stream chat rebuilds thinking DOM mid-stream so the header
// can briefly be absent). Toggling the `expanded` class directly is the
// fallback so ESC never bypasses the thinking block to hit a modal.
if (_closeHoveredWindow()) {
e.stopImmediatePropagation(); e.preventDefault();
return;
}
// Transient ad-hoc menus (dropdowns / context popups) live outside the
// .modal system and register a dismiss callback in escMenuStack. Close the
// most-recently-opened one first — so a menu opened over a modal dismisses
// before the modal — and do it BEFORE the text-input guard below, since a
// menu may own the focused input (e.g. a search dropdown).
if (dismissTopMenu()) {
if (dismissTopEscapeLayer()) {
e.stopImmediatePropagation(); e.preventDefault();
return;
}
// The event editor is an inner Calendar layer. Cancel it through the
// form's existing button before hovered-window handling can close the
// entire Calendar modal.
const calendarModal = document.getElementById('calendar-modal');
const calendarEventForm = calendarModal?.querySelector('.cal-form');
if (calendarEventForm && _isVisible(calendarModal)) {
e.stopImmediatePropagation(); e.preventDefault();
try {
(calendarEventForm.querySelector('#cal-f-cancel, #cal-form-mobile-cancel'))?.click();
} catch {}
return;
}
// Selection mode is rendered as a visible bulk bar in several modules.
// Keep it as an inner layer even where the module predates the shared
// registry, so Escape cancels selection instead of closing the library.
const selectionBars = [...document.querySelectorAll(
'.memory-bulk-bar:not(.hidden), .session-bulk-bar:not(.hidden)',
)]
.filter(bar => _isVisible(bar));
const selectionBar = selectionBars[selectionBars.length - 1];
let selectionCancel = selectionBar?.querySelector(
'button[id*="cancel"], button[title*="Cancel"], .memory-bulk-cancel',
);
// A few older library panes use the Select toggle itself as the cancel
// control and do not render a separate button in the bulk bar.
if (!selectionCancel && selectionBar) {
selectionCancel = [
'#doclib-select-btn', '#doclib-chats-select-btn',
'#doclib-arc-select-btn', '#doclib-research-select-btn',
'#memory-select-btn', '#notes-select-btn', '#skills-select-btn',
'#tasks-select-btn', '#session-select-btn', '#lib-select-btn',
'#archive-select-btn', '#email-lib-select-btn',
].map(selector => document.querySelector(selector))
.find(button => button && _isVisible(button) && /^cancel$/i.test(button.textContent.trim()));
}
if (selectionCancel) {
e.stopImmediatePropagation(); e.preventDefault();
try { selectionCancel.click(); } catch {}
return;
}
// Bulk selection is an inner Gallery layer, so cancel it before the
// generic modal handler gets a chance to close the Gallery.
if (document.getElementById('gallery-modal') && window.__galleryCancelSelection?.()) {
e.stopImmediatePropagation(); e.preventDefault();
return;
}
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
const expanded = document.querySelector('.doclib-card-expanded');
// Registered card layers handle the normal path. Keep a narrow fallback
// for older/rebuilt rows whose module has not wired the helper yet.
const expanded = document.querySelector(
'.doclib-card-expanded, .task-card.expanded, .task-log-row.expanded',
);
const think = document.querySelector('.thinking-content.expanded');
if (expanded) {
e.stopImmediatePropagation(); e.preventDefault();
@@ -1293,6 +1459,13 @@ if (!window._odyEscExpandGuard) {
}
return;
}
// Only close the hovered outer window after its inner layers have had a
// chance to handle Escape. In particular, an expanded library card must
// collapse on the first press rather than losing the whole library.
if (_closeHoveredWindow()) {
e.stopImmediatePropagation(); e.preventDefault();
return;
}
const galleryEditor = document.getElementById('gallery-editor-container');
const galleryModal = galleryEditor?.closest('.modal');
const galleryEditing = !!(