Support mobile enter for queued agent prompts

This commit is contained in:
pewdiepie-archdaemon
2026-07-01 14:23:42 +00:00
parent dc3530b8fa
commit 265f0911d5
2 changed files with 169 additions and 25 deletions
+138 -10
View File
@@ -53,6 +53,69 @@ window.uiModule = uiModule;
window.adminModule = adminModule;
window.cookbookModule = cookbookModule;
function _isMobileChatInput() {
return window.innerWidth <= 768 || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
}
function _isForegroundChatBusy() {
const sendBtn = document.querySelector('.send-btn');
return !!window.__odysseusChatBusy
|| Date.now() < (window.__odysseusChatBusyUntil || 0)
|| !!document.querySelector('.send-btn[data-mode="streaming"], .send-btn.send-pending')
|| (sendBtn && (sendBtn.title || '').toLowerCase().includes('stop'));
}
function _shouldQueueFromMobileEnter(e, input) {
return e.key === 'Enter'
&& !e.shiftKey
&& !e.ctrlKey
&& !e.metaKey
&& !e.altKey
&& !e.isComposing
&& _isMobileChatInput()
&& _isForegroundChatBusy()
&& !!(input && input.value && input.value.trim());
}
function _shouldQueueFromMobileLineBreak(input) {
return _isMobileChatInput()
&& _isForegroundChatBusy()
&& !!(input && input.value && input.value.trim());
}
function _isLineBreakInputEvent(e) {
return e
&& (e.inputType === 'insertLineBreak'
|| e.inputType === 'insertParagraph'
|| e.data === '\n');
}
function _submitMobileQueuedInput(input) {
if (!input || !_shouldQueueFromMobileLineBreak(input)) return false;
const now = Date.now();
const last = Number(input.dataset.mobileQueueSubmitAt || 0);
if (now - last < 300) return true;
input.dataset.mobileQueueSubmitAt = String(now);
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
return true;
}
window.__odysseusQueueStreamingSubmit = now;
const form = document.getElementById('chat-form');
const submitBtn = form && form.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.click();
else if (form) form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
return true;
}
function _syncMobileEnterKeyHint(input) {
if (!input) return;
input.setAttribute('enterkeyhint', (_isMobileChatInput() && _isForegroundChatBusy()) ? 'send' : 'enter');
}
function _countLineBreaks(s) {
return ((s || '').match(/\n/g) || []).length;
}
function initForegroundActivityHeartbeat() {
let lastSent = 0;
const minGapMs = 12000;
@@ -3226,17 +3289,38 @@ function initializeEventListeners() {
// Textarea auto-resize
const textarea = el('message');
if (textarea) {
_syncMobileEnterKeyHint(textarea);
window.addEventListener('odysseus:chat-busy-change', () => _syncMobileEnterKeyHint(textarea));
uiModule.autoResize(textarea);
textarea.addEventListener('input', () => {
let previousTextareaValue = textarea.value || '';
textarea.addEventListener('beforeinput', (e) => {
if (_isLineBreakInputEvent(e) && _shouldQueueFromMobileLineBreak(textarea)) {
e.preventDefault();
e.stopPropagation();
_submitMobileQueuedInput(textarea);
}
});
textarea.addEventListener('input', (e) => {
const currentValue = textarea.value || '';
const insertedLineBreak = _isLineBreakInputEvent(e)
|| _countLineBreaks(currentValue) > _countLineBreaks(previousTextareaValue);
if (insertedLineBreak && _shouldQueueFromMobileLineBreak(textarea)) {
textarea.value = currentValue.replace(/\n+$/g, '');
previousTextareaValue = textarea.value || '';
_submitMobileQueuedInput(textarea);
return;
}
previousTextareaValue = currentValue;
uiModule.autoResize(textarea);
_syncMobileEnterKeyHint(textarea);
});
textarea.addEventListener('paste', () => {
setTimeout(() => uiModule.autoResize(textarea), 1);
});
textarea.addEventListener('keydown', (e) => {
const isMobile = window.innerWidth <= 768
const isMobile = _isMobileChatInput();
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile) {
if (_shouldQueueFromMobileEnter(e, textarea) || (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile)) {
// If ghost autocomplete is active, accept the suggestion instead of submitting
if (window._ghostAutocomplete && window._ghostAutocomplete.isActive()) {
e.preventDefault();
@@ -3249,8 +3333,14 @@ function initializeEventListeners() {
// Check if already submitting before triggering form submission
const form = el('chat-form');
if (form) {
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.click();
if (_isForegroundChatBusy() && textarea.value && textarea.value.trim()) {
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
return;
}
window.__odysseusQueueStreamingSubmit = Date.now();
}
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.click();
}
}
});
@@ -3685,10 +3775,31 @@ function startOdysseusApp() {
return fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount() > 0;
}
function _updateStreamingSubmitButton() {
if (!sendBtn || sendBtn.dataset.mode !== 'streaming') return false;
const hasText = messageInput && messageInput.value.trim().length > 0;
const nextPhase = hasText ? 'queue' : 'processing';
if (sendBtn.dataset.phase === nextPhase) return true;
sendBtn.dataset.phase = nextPhase;
sendBtn.classList.remove('mic-mode', 'newchat-mode', 'newchat-expanded', 'anim-spin', 'anim-launch', 'anim-land');
if (hasText) {
sendBtn.innerHTML = _sendIcon;
sendBtn.title = 'Queue message';
} else {
sendBtn.innerHTML = _stopIcon;
sendBtn.title = 'Stop generation';
}
return true;
}
function _updateSendBtnIcon() {
if (!sendBtn) return;
// Don't override if streaming (stop button) or recording
if (sendBtn.dataset.mode === 'streaming' || sendBtn.dataset.mode === 'recording') return;
if (sendBtn.dataset.mode === 'streaming') {
_updateStreamingSubmitButton();
return;
}
// Don't override if recording
if (sendBtn.dataset.mode === 'recording') return;
const prevMode = sendBtn.dataset.mode || '';
const hasText = messageInput && messageInput.value.trim().length > 0;
const hasFiles = _hasAttachments();
@@ -3784,6 +3895,12 @@ function startOdysseusApp() {
const hasText = messageInput && messageInput.value.trim().length > 0;
const hasFiles = _hasAttachments();
if (sendBtn.dataset.mode === 'streaming') {
if (hasText) window.__odysseusQueueStreamingSubmit = Date.now();
handleSubmit(e);
return;
}
// New chat mode — empty input, no attachments, no STT
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
if (sessionModule) {
@@ -3823,9 +3940,10 @@ function startOdysseusApp() {
// Enter to send (shift+enter for newline), or new chat when empty
if (messageInput) {
messageInput.addEventListener('keydown', (e) => {
const isMobile = window.innerWidth <= 768
if (e.defaultPrevented) return;
const isMobile = _isMobileChatInput();
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile) {
if (_shouldQueueFromMobileEnter(e, messageInput) || (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile)) {
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
@@ -3836,6 +3954,12 @@ function startOdysseusApp() {
if (railNew) railNew.click();
return;
}
if (_isForegroundChatBusy() && messageInput.value && messageInput.value.trim()) {
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
return;
}
window.__odysseusQueueStreamingSubmit = Date.now();
}
handleSubmit(e);
}
});
@@ -3855,7 +3979,11 @@ function startOdysseusApp() {
_syncModelPickerAutohide();
messageInput.addEventListener('input', () => {
_syncModelPickerAutohide();
_debouncedUpdateIcon();
if (sendBtn && sendBtn.dataset.mode === 'streaming') {
_updateSendBtnIcon();
} else {
_debouncedUpdateIcon();
}
}, { passive: true });
}
+31 -15
View File
@@ -48,6 +48,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
try {
window.__odysseusChatBusy = !!active;
window.__odysseusChatBusyUntil = active ? Date.now() + 120000 : Date.now() + 1200;
window.dispatchEvent(new CustomEvent('odysseus:chat-busy-change', { detail: { active: !!active } }));
} catch (_) {}
}
let _pendingContinue = null; // Stores the stopped AI element to merge with new response
@@ -326,7 +327,12 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
// arrow out for the stop icon — otherwise the swap happens mid-flight
// and the user sees nothing fly out.
setTimeout(() => {
submitBtn.innerHTML = _stopSvg;
if (submitBtn.dataset.mode !== 'streaming') return;
const msgInput = uiModule.el('message');
const hasQueuedText = !!(msgInput && msgInput.value && msgInput.value.trim());
submitBtn.innerHTML = hasQueuedText && icons ? icons.send : _stopSvg;
submitBtn.dataset.phase = hasQueuedText ? 'queue' : 'processing';
submitBtn.title = hasQueuedText ? 'Queue message' : 'Stop generation';
submitBtn.classList.remove('anim-launch');
void submitBtn.offsetWidth;
submitBtn.classList.add('anim-land');
@@ -471,6 +477,24 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
return true;
}
export function queueStreamingComposerRequest() {
if (!isStreaming) return false;
const queuedInput = uiModule.el('message');
const queuedText = (queuedInput && queuedInput.value || '').trim();
if (!queuedText) return false;
if (fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount()) {
try { uiModule.showError && uiModule.showError('Finish the current response before queueing messages with attachments.'); } catch (_) {}
return true;
}
if (_queueAgentRequest(queuedText)) {
queuedInput.value = '';
queuedInput.dispatchEvent(new Event('input', { bubbles: true }));
if (uiModule.autoResize) uiModule.autoResize(queuedInput);
try { window._updateSendBtnIcon && window._updateSendBtnIcon(); } catch (_) {}
}
return true;
}
function _drainQueuedAgentRequests() {
if (isStreaming || _sendInFlight || !_queuedAgentRequests.length) return;
if (_queuedDrainTimer) return;
@@ -507,21 +531,13 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
return;
}
// If currently streaming, a non-empty composer means "queue this next".
// Empty composer keeps the existing Stop behavior.
// If currently streaming, keyboard Enter can queue a non-empty composer.
// Clicking the stop icon should still stop normally, even if text exists.
if (isStreaming) {
const queuedInput = uiModule.el('message');
const queuedText = (queuedInput && queuedInput.value || '').trim();
if (queuedText) {
if (fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount()) {
try { uiModule.showError && uiModule.showError('Finish the current response before queueing messages with attachments.'); } catch (_) {}
return;
}
if (_queueAgentRequest(queuedText)) {
queuedInput.value = '';
queuedInput.dispatchEvent(new Event('input', { bubbles: true }));
if (uiModule.autoResize) uiModule.autoResize(queuedInput);
}
const queueRequestedAt = Number(window.__odysseusQueueStreamingSubmit || 0);
const shouldQueueStreamingSubmit = queueRequestedAt && Date.now() - queueRequestedAt < 1200;
window.__odysseusQueueStreamingSubmit = 0;
if (shouldQueueStreamingSubmit && queueStreamingComposerRequest()) {
return;
}
if (fileHandlerModule.isUploading && fileHandlerModule.isUploading()) {