Add queued sending for Codex App drafts
Also include WebSocket heartbeat handling to keep idle connections healthy.
This commit is contained in:
623
public/app.js
623
public/app.js
@@ -2,7 +2,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const ASSET_VERSION = '20260621-cross-reply-collapse-last-section-offset';
|
||||
const ASSET_VERSION = '20260622-queued-send';
|
||||
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
|
||||
const RENDER_DEBOUNCE = 100;
|
||||
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
|
||||
@@ -196,8 +196,11 @@
|
||||
let pendingSessionSwitchRequest = null;
|
||||
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
|
||||
let pendingInitialSessionLoad = false;
|
||||
let initialSessionListHandled = false;
|
||||
let noteMode = false;
|
||||
let noteDraftSeq = 0;
|
||||
let queuedMessageSeq = 0;
|
||||
let queuedMessageDrainTimer = null;
|
||||
let isReloadingMcp = false;
|
||||
let sessionSearchQuery = '';
|
||||
const collapsedProjectKeys = (() => {
|
||||
@@ -217,6 +220,7 @@
|
||||
}
|
||||
})();
|
||||
const pendingNotesByTarget = new Map();
|
||||
const queuedMessagesByTarget = new Map();
|
||||
const userMessageIndex = new Map();
|
||||
const expandedOldSessionGroups = new Set();
|
||||
document.documentElement.dataset.dividerTime = showAgentDividerTime ? 'show' : 'hide';
|
||||
@@ -261,6 +265,7 @@
|
||||
const msgInput = $('#msg-input');
|
||||
const inputWrapper = msgInput.closest('.input-wrapper');
|
||||
const noteModeBtn = $('#note-mode-btn');
|
||||
const queueSendBtn = $('#queue-send-btn');
|
||||
const sendBtn = $('#send-btn');
|
||||
const abortBtn = $('#abort-btn');
|
||||
const cmdMenu = $('#cmd-menu');
|
||||
@@ -301,6 +306,22 @@
|
||||
return currentSessionId || getDraftNoteKey(agent);
|
||||
}
|
||||
|
||||
function getSessionQueueKey(sessionId) {
|
||||
return sessionId ? `session:${sessionId}` : '';
|
||||
}
|
||||
|
||||
function getDraftQueueKey(agent = currentAgent) {
|
||||
return `queue:${getDraftNoteKey(agent)}`;
|
||||
}
|
||||
|
||||
function getCurrentQueueKey(agent = currentAgent) {
|
||||
return currentSessionId ? getSessionQueueKey(currentSessionId) : getDraftQueueKey(agent);
|
||||
}
|
||||
|
||||
function supportsQueuedSend(agent = currentAgent) {
|
||||
return isCodexAppAgent(agent);
|
||||
}
|
||||
|
||||
function getNotesForKey(key, create = true) {
|
||||
if (!key) return [];
|
||||
if (!pendingNotesByTarget.has(key)) {
|
||||
@@ -319,6 +340,24 @@
|
||||
if (!notes || notes.length === 0) pendingNotesByTarget.delete(key);
|
||||
}
|
||||
|
||||
function getQueueForKey(key, create = true) {
|
||||
if (!key) return [];
|
||||
if (!queuedMessagesByTarget.has(key)) {
|
||||
if (!create) return [];
|
||||
queuedMessagesByTarget.set(key, []);
|
||||
}
|
||||
return queuedMessagesByTarget.get(key);
|
||||
}
|
||||
|
||||
function getCurrentQueue(create = true) {
|
||||
return getQueueForKey(getCurrentQueueKey(), create);
|
||||
}
|
||||
|
||||
function cleanupQueueKey(key) {
|
||||
const queue = queuedMessagesByTarget.get(key);
|
||||
if (!queue || queue.length === 0) queuedMessagesByTarget.delete(key);
|
||||
}
|
||||
|
||||
function migratePendingNotesToSession(sessionId, agent = currentAgent) {
|
||||
if (!sessionId) return;
|
||||
const draftKey = getDraftNoteKey(agent);
|
||||
@@ -329,6 +368,16 @@
|
||||
pendingNotesByTarget.delete(draftKey);
|
||||
}
|
||||
|
||||
function migrateQueuedMessagesToSession(sessionId, agent = currentAgent) {
|
||||
if (!sessionId) return;
|
||||
const draftKey = getDraftQueueKey(agent);
|
||||
const draftQueue = queuedMessagesByTarget.get(draftKey);
|
||||
if (!draftQueue || draftQueue.length === 0) return;
|
||||
const sessionQueue = getQueueForKey(getSessionQueueKey(sessionId), true);
|
||||
sessionQueue.push(...draftQueue);
|
||||
queuedMessagesByTarget.delete(draftKey);
|
||||
}
|
||||
|
||||
function updateGenerationControls() {
|
||||
const noteActive = !!noteMode;
|
||||
const allowRuntimeInsert = isGenerating && isCodexAppAgent(currentAgent) && !noteActive;
|
||||
@@ -339,6 +388,14 @@
|
||||
sendBtn.setAttribute('aria-label', sendLabel);
|
||||
sendBtn.hidden = isGenerating ? !(noteActive || allowRuntimeInsert) : false;
|
||||
}
|
||||
if (queueSendBtn) {
|
||||
const queueAvailable = noteActive && supportsQueuedSend();
|
||||
const queueLabel = isGenerating || currentSessionRunning ? '排队发送' : '排队发送(空闲时将立即发送)';
|
||||
queueSendBtn.hidden = !queueAvailable;
|
||||
queueSendBtn.disabled = !queueAvailable;
|
||||
queueSendBtn.title = queueLabel;
|
||||
queueSendBtn.setAttribute('aria-label', queueLabel);
|
||||
}
|
||||
if (abortBtn) {
|
||||
abortBtn.hidden = !isGenerating;
|
||||
}
|
||||
@@ -398,18 +455,69 @@
|
||||
editBtn.addEventListener('click', () => beginEditPendingNote(note.id));
|
||||
deleteBtn.addEventListener('click', () => removePendingNote(note.id));
|
||||
sendNoteBtn.addEventListener('click', () => sendPendingNote(note.id));
|
||||
actions.append(editBtn, deleteBtn, sendNoteBtn);
|
||||
actions.append(editBtn, deleteBtn);
|
||||
if (supportsQueuedSend()) {
|
||||
const queueNoteBtn = createNoteActionButton('queue', '排队', '加入自动发送队列');
|
||||
queueNoteBtn.addEventListener('click', () => queuePendingNote(note.id));
|
||||
actions.appendChild(queueNoteBtn);
|
||||
}
|
||||
actions.appendChild(sendNoteBtn);
|
||||
|
||||
bubble.append(meta, text, actions);
|
||||
div.append(avatar, bubble);
|
||||
return div;
|
||||
}
|
||||
|
||||
function createQueuedMessageElement(message, index, total) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'pending-note queued-message';
|
||||
div.dataset.queueId = message.id;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'note-avatar queue-avatar';
|
||||
avatar.textContent = 'Q';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'note-bubble queue-bubble';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'note-meta queue-meta';
|
||||
const waitLabel = isGenerating || currentSessionRunning ? '等待本轮结束' : '即将发送';
|
||||
meta.textContent = `队列 · 第 ${index + 1}/${total} 条 · ${waitLabel}`;
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = 'note-text';
|
||||
text.textContent = message.text;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'note-actions';
|
||||
|
||||
const upBtn = createNoteActionButton('move-up', '上移', '上移一位');
|
||||
upBtn.disabled = index <= 0;
|
||||
upBtn.addEventListener('click', () => moveQueuedMessage(message.id, -1));
|
||||
|
||||
const downBtn = createNoteActionButton('move-down', '下移', '下移一位');
|
||||
downBtn.disabled = index >= total - 1;
|
||||
downBtn.addEventListener('click', () => moveQueuedMessage(message.id, 1));
|
||||
|
||||
const editBtn = createNoteActionButton('edit', '修改');
|
||||
editBtn.addEventListener('click', () => beginEditQueuedMessage(message.id));
|
||||
|
||||
const deleteBtn = createNoteActionButton('delete', '删除');
|
||||
deleteBtn.addEventListener('click', () => removeQueuedMessage(message.id));
|
||||
|
||||
actions.append(upBtn, downBtn, editBtn, deleteBtn);
|
||||
bubble.append(meta, text, actions);
|
||||
div.append(avatar, bubble);
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderPendingNotes(options = {}) {
|
||||
if (!pendingNotesTray) return;
|
||||
pendingNotesTray.innerHTML = '';
|
||||
const notes = getCurrentNotes(false);
|
||||
if (!notes || notes.length === 0) {
|
||||
const queuedMessages = getCurrentQueue(false);
|
||||
if ((!notes || notes.length === 0) && (!queuedMessages || queuedMessages.length === 0)) {
|
||||
pendingNotesTray.hidden = true;
|
||||
if (options.updateScrollbar !== false) updateScrollbar();
|
||||
return;
|
||||
@@ -417,6 +525,9 @@
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
notes.forEach((note) => frag.appendChild(createPendingNoteElement(note)));
|
||||
queuedMessages.forEach((message, index) => {
|
||||
frag.appendChild(createQueuedMessageElement(message, index, queuedMessages.length));
|
||||
});
|
||||
pendingNotesTray.appendChild(frag);
|
||||
pendingNotesTray.hidden = false;
|
||||
if (options.scrollIntoView !== false && options.scroll !== false) {
|
||||
@@ -454,6 +565,91 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function getQueuedMessageValidationError(content) {
|
||||
if (!supportsQueuedSend()) return '排队发送仅支持 Codex App。';
|
||||
if (!String(content || '').trim()) return '排队内容不能为空。';
|
||||
if (String(content || '').trim().startsWith('/')) return '排队发送暂不支持 slash 指令。';
|
||||
return '';
|
||||
}
|
||||
|
||||
function addQueuedMessage(text, options = {}) {
|
||||
const content = String(text || '').trim();
|
||||
const validationError = getQueuedMessageValidationError(content);
|
||||
if (validationError) {
|
||||
appendError(validationError);
|
||||
return false;
|
||||
}
|
||||
const message = {
|
||||
id: `queued-${Date.now().toString(36)}-${++queuedMessageSeq}`,
|
||||
text: content,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
getCurrentQueue(true).push(message);
|
||||
if (options.render !== false) renderPendingNotes();
|
||||
scheduleQueuedMessageDrain();
|
||||
return true;
|
||||
}
|
||||
|
||||
function queueMessageFromInput() {
|
||||
const text = msgInput.value.trim();
|
||||
if (!text || isBlockingSessionLoad()) return;
|
||||
hideCmdMenu();
|
||||
hideOptionPicker();
|
||||
if (pendingAttachments.length > 0) {
|
||||
appendError('排队发送暂不支持图片附件,请先移除图片。');
|
||||
return;
|
||||
}
|
||||
if (addQueuedMessage(text)) {
|
||||
msgInput.value = '';
|
||||
autoResize();
|
||||
}
|
||||
}
|
||||
|
||||
function queuePendingNote(noteId) {
|
||||
const found = findPendingNote(noteId);
|
||||
if (!found) return;
|
||||
const text = String(found.note.text || '').trim();
|
||||
const validationError = getQueuedMessageValidationError(text);
|
||||
if (validationError) {
|
||||
appendError(validationError);
|
||||
return;
|
||||
}
|
||||
dropPendingNote(noteId);
|
||||
addQueuedMessage(text, { render: false });
|
||||
renderPendingNotes();
|
||||
}
|
||||
|
||||
function findQueuedMessage(queueId) {
|
||||
const key = getCurrentQueueKey();
|
||||
const queue = getQueueForKey(key, false);
|
||||
const index = queue.findIndex((message) => message.id === queueId);
|
||||
if (index === -1) return null;
|
||||
return { key, queue, index, message: queue[index] };
|
||||
}
|
||||
|
||||
function dropQueuedMessage(queueId) {
|
||||
const found = findQueuedMessage(queueId);
|
||||
if (!found) return null;
|
||||
const [message] = found.queue.splice(found.index, 1);
|
||||
cleanupQueueKey(found.key);
|
||||
return message;
|
||||
}
|
||||
|
||||
function removeQueuedMessage(queueId) {
|
||||
if (!dropQueuedMessage(queueId)) return;
|
||||
renderPendingNotes({ scroll: false });
|
||||
}
|
||||
|
||||
function moveQueuedMessage(queueId, delta) {
|
||||
const found = findQueuedMessage(queueId);
|
||||
if (!found) return;
|
||||
const nextIndex = found.index + delta;
|
||||
if (nextIndex < 0 || nextIndex >= found.queue.length) return;
|
||||
const [message] = found.queue.splice(found.index, 1);
|
||||
found.queue.splice(nextIndex, 0, message);
|
||||
renderPendingNotes({ scroll: false });
|
||||
}
|
||||
|
||||
function removePendingNote(noteId) {
|
||||
if (!dropPendingNote(noteId)) return;
|
||||
renderPendingNotes({ scroll: false });
|
||||
@@ -522,6 +718,65 @@
|
||||
});
|
||||
}
|
||||
|
||||
function beginEditQueuedMessage(queueId) {
|
||||
const found = findQueuedMessage(queueId);
|
||||
if (!found) return;
|
||||
const queueEl = pendingNotesTray?.querySelector(`.queued-message[data-queue-id="${queueId}"]`);
|
||||
const bubble = queueEl?.querySelector('.note-bubble');
|
||||
if (!bubble) return;
|
||||
|
||||
bubble.classList.add('editing');
|
||||
bubble.innerHTML = '';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'note-meta queue-meta';
|
||||
meta.textContent = '修改排队消息';
|
||||
|
||||
const editor = document.createElement('textarea');
|
||||
editor.className = 'note-edit-input';
|
||||
editor.value = found.message.text;
|
||||
editor.rows = 3;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'note-actions';
|
||||
const saveBtn = createNoteActionButton('save', '保存');
|
||||
const cancelBtn = createNoteActionButton('cancel', '取消');
|
||||
|
||||
const save = () => {
|
||||
const next = editor.value.trim();
|
||||
const validationError = getQueuedMessageValidationError(next);
|
||||
if (validationError) {
|
||||
appendError(validationError);
|
||||
editor.focus();
|
||||
return;
|
||||
}
|
||||
found.message.text = next;
|
||||
renderPendingNotes({ scroll: false });
|
||||
};
|
||||
|
||||
saveBtn.addEventListener('click', save);
|
||||
cancelBtn.addEventListener('click', () => renderPendingNotes({ scroll: false }));
|
||||
editor.addEventListener('input', () => resizeNoteEditor(editor));
|
||||
editor.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
save();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
renderPendingNotes({ scroll: false });
|
||||
}
|
||||
});
|
||||
|
||||
actions.append(saveBtn, cancelBtn);
|
||||
bubble.append(meta, editor, actions);
|
||||
requestAnimationFrame(() => {
|
||||
resizeNoteEditor(editor);
|
||||
editor.focus();
|
||||
editor.select();
|
||||
});
|
||||
}
|
||||
|
||||
function sendPendingNote(noteId) {
|
||||
if (isGenerating || isBlockingSessionLoad()) {
|
||||
appendError('当前回复还在生成,稍后再发送笔记。');
|
||||
@@ -535,6 +790,30 @@
|
||||
submitUserMessage(text);
|
||||
}
|
||||
|
||||
function scheduleQueuedMessageDrain() {
|
||||
if (queuedMessageDrainTimer) return;
|
||||
queuedMessageDrainTimer = setTimeout(() => {
|
||||
queuedMessageDrainTimer = null;
|
||||
drainQueuedMessages();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function drainQueuedMessages() {
|
||||
if (!supportsQueuedSend() || isGenerating || currentSessionRunning || isBlockingSessionLoad()) return;
|
||||
const queue = getCurrentQueue(false);
|
||||
if (!queue || queue.length === 0) return;
|
||||
const [message] = queue.splice(0, 1);
|
||||
cleanupQueueKey(getCurrentQueueKey());
|
||||
renderPendingNotes({ scroll: false });
|
||||
|
||||
const text = String(message?.text || '').trim();
|
||||
if (!text) {
|
||||
scheduleQueuedMessageDrain();
|
||||
return;
|
||||
}
|
||||
submitUserMessage(text);
|
||||
}
|
||||
|
||||
function normalizeTheme(theme) {
|
||||
return THEME_OPTIONS.some((item) => item.value === theme) ? theme : 'washi';
|
||||
}
|
||||
@@ -2865,6 +3144,7 @@
|
||||
localStorage.removeItem(getAgentSessionStorageKey(currentAgent));
|
||||
}
|
||||
pendingNotesByTarget.delete(session.id);
|
||||
queuedMessagesByTarget.delete(getSessionQueueKey(session.id));
|
||||
invalidateSessionCache(session.id);
|
||||
send({ type: 'delete_session', sessionId: session.id });
|
||||
if (session.id === currentSessionId) {
|
||||
@@ -2945,6 +3225,7 @@
|
||||
}
|
||||
}
|
||||
updateCwdBadge();
|
||||
if (!running) scheduleQueuedMessageDrain();
|
||||
}
|
||||
|
||||
function updateAgentScopedUI() {
|
||||
@@ -3047,6 +3328,7 @@
|
||||
updateSessionIdBadge();
|
||||
setCurrentAgent(snapshotAgent);
|
||||
migratePendingNotesToSession(snapshot.sessionId, snapshotAgent);
|
||||
migrateQueuedMessagesToSession(snapshot.sessionId, snapshotAgent);
|
||||
setCurrentSessionRunningState(snapshot.isRunning);
|
||||
setStatsDisplay(snapshot);
|
||||
closeUserOutlinePanel();
|
||||
@@ -3285,22 +3567,69 @@
|
||||
|
||||
// --- marked config ---
|
||||
const PREVIEW_LANGS = new Set(['html', 'svg']);
|
||||
const MERMAID_LANGS = new Set(['mermaid', 'mmd']);
|
||||
const _previewCodeMap = new Map();
|
||||
let _previewCodeId = 0;
|
||||
let _mermaidInitialized = false;
|
||||
let _mermaidRenderId = 0;
|
||||
|
||||
function normalizeCodeLanguage(language) {
|
||||
const lang = String(language || '').trim().split(/\s+/)[0].toLowerCase();
|
||||
return lang || 'plaintext';
|
||||
}
|
||||
|
||||
function highlightCodeBlock(code, lang) {
|
||||
try {
|
||||
if (hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
} catch {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
}
|
||||
|
||||
function getCodeBlockSource(wrapper) {
|
||||
if (!wrapper) return '';
|
||||
const cid = wrapper.dataset.cid ? Number(wrapper.dataset.cid) : 0;
|
||||
if (cid && _previewCodeMap.has(cid)) return _previewCodeMap.get(cid);
|
||||
return wrapper.querySelector('code')?.textContent || '';
|
||||
}
|
||||
|
||||
function createStoredCodeId(code) {
|
||||
const cid = ++_previewCodeId;
|
||||
_previewCodeMap.set(cid, code);
|
||||
return cid;
|
||||
}
|
||||
|
||||
function renderMermaidCodeBlock(code, lang) {
|
||||
const cid = createStoredCodeId(code);
|
||||
const highlighted = highlightCodeBlock(code, lang);
|
||||
return `<div class="code-block-wrapper mermaid-code-block mermaid-diagram-mode" data-cid="${cid}">
|
||||
<div class="code-block-header">
|
||||
<span>${escapeHtml(lang)}</span>
|
||||
<div class="code-block-actions">
|
||||
<button class="code-preview-btn mermaid-toggle-btn" type="button" onclick="ccToggleMermaid(this, event)" aria-label="切换 Mermaid 图形与代码">代码</button>
|
||||
<button class="code-preview-btn mermaid-copy-graph-btn" type="button" onclick="ccCopyMermaidDiagram(this, event)" aria-label="复制 Mermaid 图形">复制图</button>
|
||||
<button class="code-preview-btn mermaid-download-btn" type="button" onclick="ccDownloadMermaidDiagram(this, event)" aria-label="下载 Mermaid 图形">下载图</button>
|
||||
<button class="code-copy-btn" type="button" onclick="ccCopyCode(this, event)" aria-label="复制 Mermaid 代码">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mermaid-render-pane" data-render-state="pending">
|
||||
<div class="mermaid-render-target" aria-label="Mermaid 图形"></div>
|
||||
<div class="mermaid-render-error" role="alert" hidden></div>
|
||||
</div>
|
||||
<pre><code class="hljs language-${escapeHtml(lang)}">${highlighted}</code></pre>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.code = function (code, language) {
|
||||
const lang = (language || 'plaintext').toLowerCase();
|
||||
let highlighted;
|
||||
try {
|
||||
if (hljs.getLanguage(lang)) {
|
||||
highlighted = hljs.highlight(code, { language: lang }).value;
|
||||
} else {
|
||||
highlighted = hljs.highlightAuto(code).value;
|
||||
}
|
||||
} catch {
|
||||
highlighted = escapeHtml(code);
|
||||
}
|
||||
const source = String(code || '');
|
||||
const lang = normalizeCodeLanguage(language);
|
||||
if (MERMAID_LANGS.has(lang)) return renderMermaidCodeBlock(source, lang);
|
||||
|
||||
const highlighted = highlightCodeBlock(source, lang);
|
||||
const canPreview = PREVIEW_LANGS.has(lang);
|
||||
const previewBtn = canPreview
|
||||
? `<button class="code-preview-btn" type="button" onclick="ccTogglePreview(this, event)" aria-label="预览代码块">Preview</button>`
|
||||
@@ -3308,8 +3637,7 @@
|
||||
const previewPane = canPreview
|
||||
? `<div class="code-preview-pane"><iframe class="code-preview-iframe" sandbox="allow-scripts" loading="lazy"></iframe></div>`
|
||||
: '';
|
||||
const cid = canPreview ? (++_previewCodeId) : 0;
|
||||
if (canPreview) _previewCodeMap.set(cid, code);
|
||||
const cid = canPreview ? createStoredCodeId(source) : 0;
|
||||
return `<div class="code-block-wrapper${canPreview ? ' has-preview' : ''}"${canPreview ? ` data-cid="${cid}"` : ''}>
|
||||
<div class="code-block-header">
|
||||
<span>${escapeHtml(lang)}</span>
|
||||
@@ -3325,8 +3653,7 @@
|
||||
event?.stopPropagation();
|
||||
const wrapper = btn?.closest?.('.code-block-wrapper');
|
||||
if (!wrapper) return;
|
||||
const cid = wrapper.dataset.cid ? Number(wrapper.dataset.cid) : 0;
|
||||
const code = (cid && _previewCodeMap.has(cid)) ? _previewCodeMap.get(cid) : wrapper.querySelector('code')?.textContent;
|
||||
const code = getCodeBlockSource(wrapper);
|
||||
const defaultLabel = btn.dataset.defaultLabel || btn.textContent || 'Copy';
|
||||
btn.dataset.defaultLabel = defaultLabel;
|
||||
btn.disabled = true;
|
||||
@@ -3361,6 +3688,238 @@
|
||||
}
|
||||
};
|
||||
|
||||
function ensureMermaidInitialized() {
|
||||
if (!window.mermaid?.render) throw new Error('Mermaid 加载失败');
|
||||
if (!_mermaidInitialized) {
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: 'default',
|
||||
});
|
||||
_mermaidInitialized = true;
|
||||
}
|
||||
return window.mermaid;
|
||||
}
|
||||
|
||||
function setMermaidRenderError(wrapper, message) {
|
||||
const pane = wrapper?.querySelector?.('.mermaid-render-pane');
|
||||
const errorEl = wrapper?.querySelector?.('.mermaid-render-error');
|
||||
if (!pane || !errorEl) return;
|
||||
pane.dataset.renderState = 'error';
|
||||
errorEl.hidden = false;
|
||||
errorEl.textContent = message || 'Mermaid 图形渲染失败';
|
||||
}
|
||||
|
||||
async function hydrateMermaidBlock(wrapper, force = false) {
|
||||
if (!wrapper || (!force && wrapper.dataset.mermaidRendered === '1')) return true;
|
||||
const pane = wrapper.querySelector('.mermaid-render-pane');
|
||||
const target = wrapper.querySelector('.mermaid-render-target');
|
||||
const errorEl = wrapper.querySelector('.mermaid-render-error');
|
||||
if (!pane || !target) return false;
|
||||
|
||||
const code = getCodeBlockSource(wrapper).trim();
|
||||
if (!code) {
|
||||
setMermaidRenderError(wrapper, 'Mermaid 代码为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderKey = String(++_mermaidRenderId);
|
||||
wrapper.dataset.mermaidRenderKey = renderKey;
|
||||
wrapper.dataset.mermaidRendered = '0';
|
||||
pane.dataset.renderState = 'pending';
|
||||
target.innerHTML = '';
|
||||
if (errorEl) {
|
||||
errorEl.hidden = true;
|
||||
errorEl.textContent = '';
|
||||
}
|
||||
|
||||
try {
|
||||
const mermaidApi = ensureMermaidInitialized();
|
||||
const result = await mermaidApi.render(`ccweb-mermaid-${renderKey}`, code);
|
||||
if (!wrapper.isConnected || wrapper.dataset.mermaidRenderKey !== renderKey) return false;
|
||||
target.innerHTML = typeof result === 'string' ? result : result.svg;
|
||||
if (typeof result?.bindFunctions === 'function') result.bindFunctions(target);
|
||||
pane.dataset.renderState = 'rendered';
|
||||
wrapper.dataset.mermaidRendered = '1';
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!wrapper.isConnected || wrapper.dataset.mermaidRenderKey !== renderKey) return false;
|
||||
setMermaidRenderError(wrapper, error?.message || 'Mermaid 图形渲染失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateMermaidBlocks(root) {
|
||||
if (!root) return;
|
||||
const blocks = root.matches?.('.mermaid-code-block')
|
||||
? [root]
|
||||
: Array.from(root.querySelectorAll('.mermaid-code-block'));
|
||||
blocks.forEach((block) => {
|
||||
hydrateMermaidBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
function hydrateRenderedMarkdown(root) {
|
||||
hydrateMermaidBlocks(root);
|
||||
}
|
||||
|
||||
function setRenderedMarkdown(target, text, hydrate = true) {
|
||||
target.innerHTML = renderMarkdown(text);
|
||||
if (hydrate) hydrateRenderedMarkdown(target);
|
||||
}
|
||||
|
||||
window.ccToggleMermaid = function (btn, event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const wrapper = btn?.closest?.('.mermaid-code-block');
|
||||
if (!wrapper) return;
|
||||
const showCode = wrapper.classList.contains('mermaid-diagram-mode');
|
||||
wrapper.classList.toggle('mermaid-diagram-mode', !showCode);
|
||||
wrapper.classList.toggle('mermaid-source-mode', showCode);
|
||||
btn.textContent = showCode ? '图' : '代码';
|
||||
if (!showCode) hydrateMermaidBlock(wrapper);
|
||||
};
|
||||
|
||||
function serializeMermaidSvg(wrapper) {
|
||||
const svg = wrapper?.querySelector?.('.mermaid-render-target svg');
|
||||
if (!svg) return '';
|
||||
const clone = svg.cloneNode(true);
|
||||
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
if (!clone.getAttribute('width') || !clone.getAttribute('height')) {
|
||||
const viewBox = clone.getAttribute('viewBox');
|
||||
const parts = viewBox ? viewBox.trim().split(/\s+/).map(Number) : [];
|
||||
if (parts.length === 4 && parts.every(Number.isFinite)) {
|
||||
clone.setAttribute('width', String(Math.max(1, Math.ceil(parts[2]))));
|
||||
clone.setAttribute('height', String(Math.max(1, Math.ceil(parts[3]))));
|
||||
}
|
||||
}
|
||||
return new XMLSerializer().serializeToString(clone);
|
||||
}
|
||||
|
||||
async function getMermaidSvgText(wrapper) {
|
||||
if (!serializeMermaidSvg(wrapper)) {
|
||||
await hydrateMermaidBlock(wrapper, true);
|
||||
}
|
||||
return serializeMermaidSvg(wrapper);
|
||||
}
|
||||
|
||||
function svgTextToPngBlob(svgText) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const width = Math.max(1, image.naturalWidth || image.width || 1200);
|
||||
const height = Math.max(1, image.naturalHeight || image.height || 800);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
canvas.toBlob((blob) => {
|
||||
URL.revokeObjectURL(url);
|
||||
if (blob) resolve(blob);
|
||||
else reject(new Error('PNG 导出失败'));
|
||||
}, 'image/png');
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('SVG 图形读取失败'));
|
||||
};
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
async function copyBlobToClipboard(blob, type) {
|
||||
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined' || !window.isSecureContext) return false;
|
||||
await navigator.clipboard.write([new ClipboardItem({ [type]: blob })]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setTemporaryButtonLabel(btn, label, fallback, timeout = 1500) {
|
||||
if (!btn) return;
|
||||
const defaultLabel = btn.dataset.defaultLabel || fallback || btn.textContent || '';
|
||||
btn.dataset.defaultLabel = defaultLabel;
|
||||
if (btn._labelResetTimer) clearTimeout(btn._labelResetTimer);
|
||||
btn.textContent = label;
|
||||
btn._labelResetTimer = setTimeout(() => {
|
||||
btn.textContent = btn.dataset.defaultLabel || defaultLabel;
|
||||
btn._labelResetTimer = null;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
window.ccCopyMermaidDiagram = async function (btn, event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const wrapper = btn?.closest?.('.mermaid-code-block');
|
||||
if (!wrapper) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const svgText = await getMermaidSvgText(wrapper);
|
||||
if (!svgText) throw new Error('暂无可复制图形');
|
||||
let copied = false;
|
||||
try {
|
||||
const pngBlob = await svgTextToPngBlob(svgText);
|
||||
copied = await copyBlobToClipboard(pngBlob, 'image/png');
|
||||
} catch {}
|
||||
if (!copied) {
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
try { copied = await copyBlobToClipboard(svgBlob, 'image/svg+xml'); } catch {}
|
||||
}
|
||||
if (copied) {
|
||||
showToast('图形已复制');
|
||||
setTemporaryButtonLabel(btn, '已复制', '复制图');
|
||||
} else {
|
||||
await copyTextToClipboard(svgText, '图形 SVG 已复制');
|
||||
setTemporaryButtonLabel(btn, '已复制', '复制图');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error?.message || '复制图形失败');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.rel = 'noopener';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
window.ccDownloadMermaidDiagram = async function (btn, event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const wrapper = btn?.closest?.('.mermaid-code-block');
|
||||
if (!wrapper) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const svgText = await getMermaidSvgText(wrapper);
|
||||
if (!svgText) throw new Error('暂无可下载图形');
|
||||
const cid = wrapper.dataset.cid || Date.now();
|
||||
const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
downloadBlob(blob, `mermaid-${cid}.svg`);
|
||||
showToast('图形已下载');
|
||||
setTemporaryButtonLabel(btn, '已下载', '下载图');
|
||||
} catch (error) {
|
||||
showToast(error?.message || '下载图形失败');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- WebSocket ---
|
||||
function isBrowserOnline() {
|
||||
return !('onLine' in navigator) || navigator.onLine;
|
||||
@@ -3449,6 +4008,7 @@
|
||||
switch (msg.type) {
|
||||
case 'auth_result':
|
||||
if (msg.success) {
|
||||
const shouldLoadInitialSession = !initialSessionListHandled && !currentSessionId;
|
||||
authToken = msg.token;
|
||||
wsAuthenticated = true;
|
||||
localStorage.setItem('cc-web-token', msg.token);
|
||||
@@ -3461,7 +4021,7 @@
|
||||
if (msg.mustChangePassword) {
|
||||
showForceChangePassword();
|
||||
} else {
|
||||
pendingInitialSessionLoad = true;
|
||||
pendingInitialSessionLoad = shouldLoadInitialSession;
|
||||
}
|
||||
} else {
|
||||
pendingSessionSwitchRequest = null;
|
||||
@@ -3493,9 +4053,13 @@
|
||||
}
|
||||
if (pendingInitialSessionLoad) {
|
||||
pendingInitialSessionLoad = false;
|
||||
initialSessionListHandled = true;
|
||||
syncViewForAgent(currentAgent, { preserveCurrent: false, loadLast: true });
|
||||
} else if (currentSessionId && !getSessionMeta(currentSessionId)) {
|
||||
initialSessionListHandled = true;
|
||||
resetChatView(currentAgent);
|
||||
} else {
|
||||
initialSessionListHandled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -3907,6 +4471,7 @@
|
||||
toolGroupCount = 0;
|
||||
hasGrouped = false;
|
||||
updateNoteModeUI();
|
||||
renderPendingNotes({ scroll: false });
|
||||
// 不禁用输入框,允许用户继续输入(但无法发送)
|
||||
|
||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||
@@ -3980,6 +4545,7 @@
|
||||
|
||||
if (sessionId) {
|
||||
migratePendingNotesToSession(sessionId, currentAgent);
|
||||
migrateQueuedMessagesToSession(sessionId, currentAgent);
|
||||
}
|
||||
pendingText = '';
|
||||
activeToolCalls.clear();
|
||||
@@ -3987,6 +4553,7 @@
|
||||
toolGroupCount = 0;
|
||||
hasGrouped = false;
|
||||
renderPendingNotes();
|
||||
scheduleQueuedMessageDrain();
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
@@ -4010,7 +4577,7 @@
|
||||
} else if (pendingText) {
|
||||
let textDiv = bubble.querySelector('.msg-text');
|
||||
if (!textDiv) { textDiv = bubble; }
|
||||
textDiv.innerHTML = renderMarkdown(pendingText);
|
||||
setRenderedMarkdown(textDiv, pendingText);
|
||||
}
|
||||
syncAssistantLastSectionButton(streamEl);
|
||||
scrollToBottom();
|
||||
@@ -4395,14 +4962,16 @@
|
||||
const after = content.substring(jsonMatch.index + jsonMatch[0].length);
|
||||
if (before.trim()) {
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.innerHTML = renderMarkdown(before);
|
||||
setRenderedMarkdown(textDiv, before, false);
|
||||
bubble.appendChild(textDiv);
|
||||
hydrateRenderedMarkdown(textDiv);
|
||||
}
|
||||
bubble.appendChild(createTodoListElement(parsed));
|
||||
if (after.trim()) {
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.innerHTML = renderMarkdown(after);
|
||||
setRenderedMarkdown(textDiv, after, false);
|
||||
bubble.appendChild(textDiv);
|
||||
hydrateRenderedMarkdown(textDiv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4420,7 +4989,7 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
bubble.innerHTML = renderMarkdown(content);
|
||||
setRenderedMarkdown(bubble, content);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4428,8 +4997,9 @@
|
||||
content.forEach(block => {
|
||||
if (block.type === 'text') {
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.innerHTML = renderMarkdown(block.text || '');
|
||||
setRenderedMarkdown(textDiv, block.text || '', false);
|
||||
bubble.appendChild(textDiv);
|
||||
hydrateRenderedMarkdown(textDiv);
|
||||
} else if (block.type === 'todo_list') {
|
||||
bubble.appendChild(createTodoListElement(block));
|
||||
}
|
||||
@@ -4437,7 +5007,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
bubble.innerHTML = renderMarkdown(String(content));
|
||||
setRenderedMarkdown(bubble, String(content));
|
||||
}
|
||||
|
||||
function createTodoListElement(block) {
|
||||
@@ -6750,6 +7320,9 @@
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
if (queueSendBtn) {
|
||||
queueSendBtn.addEventListener('click', queueMessageFromInput);
|
||||
}
|
||||
if (noteModeBtn) {
|
||||
noteModeBtn.addEventListener('click', () => {
|
||||
noteMode = !noteMode;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
document.documentElement.dataset.dividerTime = dividerTime;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css?v=20260621-cross-reply-collapse-last-section-offset">
|
||||
<link rel="stylesheet" href="style.css?v=20260622-queued-send">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -126,6 +126,14 @@
|
||||
<path d="m15 5 3 3"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="queue-send-btn" class="queue-send-btn" title="排队发送" aria-label="排队发送" type="button" hidden>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 6h10"></path>
|
||||
<path d="M4 12h8"></path>
|
||||
<path d="M4 18h10"></path>
|
||||
<path d="m16 10 4 4-4 4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="send-btn" class="send-btn" title="发送">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
@@ -154,6 +162,7 @@
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="app.js?v=20260621-cross-reply-collapse-last-section-offset"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
|
||||
<script src="app.js?v=20260622-queued-send"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
132
public/style.css
132
public/style.css
@@ -1044,6 +1044,7 @@ body.session-loading-active {
|
||||
.session-search {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
.session-search::before {
|
||||
content: '⌕';
|
||||
@@ -1058,6 +1059,9 @@ body.session-loading-active {
|
||||
pointer-events: none;
|
||||
}
|
||||
.session-search-input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 34px 0 30px;
|
||||
@@ -1073,6 +1077,13 @@ body.session-loading-active {
|
||||
.session-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.session-search-input::-webkit-search-decoration,
|
||||
.session-search-input::-webkit-search-cancel-button,
|
||||
.session-search-input::-webkit-search-results-button,
|
||||
.session-search-input::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.session-search-input:focus {
|
||||
background: var(--bg-primary);
|
||||
border-color: rgba(192, 85, 58, 0.34);
|
||||
@@ -1083,6 +1094,7 @@ body.session-loading-active {
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
z-index: 2;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2454,9 +2466,24 @@ body.session-loading-active {
|
||||
border-color: var(--note-accent);
|
||||
color: var(--note-accent);
|
||||
}
|
||||
.note-action:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
.note-action:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
.note-action.queue {
|
||||
background: var(--note-bg-strong);
|
||||
border-color: var(--note-border);
|
||||
color: var(--note-accent);
|
||||
}
|
||||
.note-action.queue:hover {
|
||||
background: var(--note-accent);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.note-action.send,
|
||||
.note-action.save {
|
||||
background: var(--note-accent);
|
||||
@@ -2723,6 +2750,63 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
.code-block-wrapper.preview-mode .code-preview-pane { display: block; }
|
||||
.code-block-wrapper.preview-mode pre { display: none; }
|
||||
|
||||
.mermaid-code-block .code-block-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mermaid-code-block.mermaid-diagram-mode pre {
|
||||
display: none;
|
||||
}
|
||||
.mermaid-code-block.mermaid-source-mode .mermaid-render-pane {
|
||||
display: none;
|
||||
}
|
||||
.mermaid-render-pane {
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: #1f2933;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mermaid-render-pane[data-render-state='pending']::before {
|
||||
content: '图形渲染中...';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 18px;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mermaid-render-pane[data-render-state='rendered']::before,
|
||||
.mermaid-render-pane[data-render-state='error']::before {
|
||||
content: none;
|
||||
}
|
||||
.mermaid-render-target {
|
||||
min-width: min-content;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mermaid-render-target svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.mermaid-render-error {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(220, 53, 69, 0.28);
|
||||
border-radius: 8px;
|
||||
background: rgba(220, 53, 69, 0.08);
|
||||
color: #b42318;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.code-block-header {
|
||||
padding: 8px 10px;
|
||||
@@ -3305,6 +3389,18 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 8px 18px rgba(45, 31, 20, 0.05);
|
||||
}
|
||||
.queued-message .queue-avatar {
|
||||
background: var(--accent);
|
||||
}
|
||||
.queued-message .queue-bubble {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.72), transparent),
|
||||
var(--accent-light);
|
||||
border-color: rgba(192, 85, 58, 0.2);
|
||||
}
|
||||
.queued-message .queue-meta {
|
||||
color: var(--accent);
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3381,8 +3477,32 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
background: var(--note-accent-hover);
|
||||
color: #fff;
|
||||
}
|
||||
.queue-send-btn {
|
||||
appearance: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: var(--info);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
|
||||
}
|
||||
.queue-send-btn:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.queue-send-btn:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
.attach-btn:disabled,
|
||||
.note-mode-btn:disabled {
|
||||
.note-mode-btn:disabled,
|
||||
.queue-send-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -3576,7 +3696,8 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
|
||||
.code-block-wrapper pre code { font-size: 12px; }
|
||||
.input-wrapper { padding: 6px 10px; border-radius: 12px; }
|
||||
.attach-btn { width: 34px; height: 34px; border-radius: 10px; }
|
||||
.note-mode-btn { width: 34px; height: 34px; border-radius: 10px; }
|
||||
.note-mode-btn,
|
||||
.queue-send-btn { width: 34px; height: 34px; border-radius: 10px; }
|
||||
.send-btn, .abort-btn { width: 34px; height: 34px; }
|
||||
.pending-notes-tray { max-height: 34vh; margin-bottom: 8px; }
|
||||
.pending-note { grid-template-columns: 26px minmax(0, 1fr); gap: 7px; }
|
||||
@@ -5413,6 +5534,7 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
||||
}
|
||||
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .note-mode-btn,
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .queue-send-btn,
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .send-btn.note-send {
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
@@ -5456,6 +5578,12 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .queued-message .queue-bubble {
|
||||
background: var(--accent-light);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .settings-inline-note code,
|
||||
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call-code {
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
|
||||
Reference in New Issue
Block a user