mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-17 13:42:21 +02:00
Merge pull request #5817 from RaresKeY/fix/agent-external-context-gate
fix(agent): gate tools after external context
This commit is contained in:
+4
-4
@@ -10,9 +10,9 @@ import modelsModule from './js/models.js?v=20260715startupcalm2';
|
||||
import ragModule from './js/rag.js';
|
||||
import presetsModule from './js/presets.js';
|
||||
import searchModule from './js/search.js';
|
||||
import chatModule from './js/chat.js?v=20260801fix1';
|
||||
import chatModule from './js/chat.js?v=20260815toolapproval4';
|
||||
import compareModule from './js/compare/index.js?v=20260723compareicon2';
|
||||
import documentModule from './js/document.js?v=20260722emailfastindex1';
|
||||
import documentModule from './js/document.js?v=20260815approvalsave1';
|
||||
import searchChatModule from './js/search-chat.js';
|
||||
import { makeWindowDraggable } from './js/windowDrag.js';
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
settleSessionHydration
|
||||
} from './js/startupShell.js';
|
||||
import markdownModule from './js/markdown.js';
|
||||
import chatRenderer from './js/chatRenderer.js?v=20260722emailfastindex1';
|
||||
import chatRenderer from './js/chatRenderer.js?v=20260815toolapproval4';
|
||||
import sessionModule from './js/sessions.js';
|
||||
import memoryModule from './js/memory.js?v=20260722memoryloading1';
|
||||
import voiceRecorderModule from './js/voiceRecorder.js';
|
||||
@@ -33,7 +33,7 @@ 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=20260722emailfastindex1';
|
||||
import settingsModule from './js/settings.js?v=20260815approvalsave1';
|
||||
// Eagerly bind unified minimize/restore behavior across all tool modals.
|
||||
import './js/modalManager.js?v=20260723compareicon2';
|
||||
// Desktop window tiling — drag a modal near an edge/corner to snap.
|
||||
|
||||
+8
-8
@@ -258,8 +258,8 @@
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-Regular.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-SemiBold.woff2">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260808startupshell1">
|
||||
<link rel="modulepreload" href="/static/app.js?v=20260808startupshell1">
|
||||
<link rel="modulepreload" href="/static/js/chat.js?v=20260801fix1">
|
||||
<link rel="modulepreload" href="/static/app.js?v=20260815toolapproval4">
|
||||
<link rel="modulepreload" href="/static/js/chat.js?v=20260815toolapproval4">
|
||||
<link rel="modulepreload" href="/static/js/ui.js">
|
||||
<link rel="modulepreload" href="/static/js/sessions.js">
|
||||
<link rel="modulepreload" href="/static/js/markdown.js">
|
||||
@@ -2532,20 +2532,20 @@
|
||||
<script type="module" src="/static/js/search.js"></script>
|
||||
<script type="module" src="/static/js/spinner.js"></script>
|
||||
<script type="module" src="/static/js/tts-ai.js"></script>
|
||||
<script type="module" src="/static/js/document.js?v=20260722emailfastindex1"></script>
|
||||
<script type="module" src="/static/js/document.js?v=20260815approvalsave1"></script>
|
||||
<script type="module" src="/static/js/gallery.js?v=20260708match1"></script>
|
||||
<script type="module" src="/static/js/chatRenderer.js?v=20260722emailfastindex1"></script>
|
||||
<script type="module" src="/static/js/chatRenderer.js?v=20260815toolapproval4"></script>
|
||||
<script type="module" src="/static/js/codeRunner.js"></script>
|
||||
<script type="module" src="/static/js/chatStream.js?v=20260722emailfastindex1"></script>
|
||||
<script type="module" src="/static/js/chat.js?v=20260801fix1"></script>
|
||||
<script type="module" src="/static/js/chatStream.js?v=20260815approvalsave1"></script>
|
||||
<script type="module" src="/static/js/chat.js?v=20260815toolapproval4"></script>
|
||||
<script type="module" src="/static/js/cookbook.js"></script>
|
||||
<script src="/static/js/cookbookSchedule.js"></script>
|
||||
<script type="module" src="/static/js/search-chat.js"></script>
|
||||
<script type="module" src="/static/js/theme.js"></script>
|
||||
<script type="module" src="/static/js/censor.js"></script>
|
||||
<script type="module" src="/static/js/settings.js?v=20260723compareicon1"></script>
|
||||
<script type="module" src="/static/js/settings.js?v=20260815approvalsave1"></script>
|
||||
<script type="module" src="/static/js/assistant.js"></script>
|
||||
<script type="module" src="/static/app.js?v=20260808startupshell1"></script> <!-- app.js must be LAST -->
|
||||
<script type="module" src="/static/app.js?v=20260815toolapproval4"></script> <!-- app.js must be LAST -->
|
||||
<script type="module" src="/static/js/init.js?v=20260715freshroot3"></script>
|
||||
<script type="module" src="/static/js/a11y.js"></script>
|
||||
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script>
|
||||
|
||||
+125
-75
@@ -8,18 +8,18 @@
|
||||
import Storage from './storage.js';
|
||||
import uiModule from './ui.js';
|
||||
import sessionModule from './sessions.js';
|
||||
import chatRenderer from './chatRenderer.js?v=20260722emailfastindex1';
|
||||
import chatStream from './chatStream.js';
|
||||
import chatRenderer from './chatRenderer.js?v=20260815toolapproval4';
|
||||
import chatStream from './chatStream.js?v=20260815approvalsave1';
|
||||
import { addAITTSButton } from './tts-ai.js';
|
||||
import markdownModule from './markdown.js';
|
||||
import spinnerModule from './spinner.js';
|
||||
import presetsModule from './presets.js';
|
||||
import fileHandlerModule from './fileHandler.js';
|
||||
import searchModule from './search.js';
|
||||
import documentModule from './document.js?v=20260722emailfastindex1';
|
||||
import * as emailInbox from './emailInbox.js?v=20260722emailfastindex1';
|
||||
import documentModule from './document.js?v=20260815approvalsave1';
|
||||
import * as emailInbox from './emailInbox.js?v=20260815approvalsave1';
|
||||
import codeRunnerModule from './codeRunner.js';
|
||||
import slashCommands, { initSlashCommands, isCommand, handleSlashCommand, handleSetupInput, handleSetupWizard, typewriterInto } from './slashCommands.js?v=20260722emailfastindex1';
|
||||
import slashCommands, { initSlashCommands, isCommand, handleSlashCommand, handleSetupInput, handleSetupWizard, typewriterInto } from './slashCommands.js?v=20260815approvalsave1';
|
||||
import createResearchSynapse from './researchSynapse.js';
|
||||
import { createStreamRenderer } from './streamingRenderer.js';
|
||||
import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArrowUpRecall.js?v=20260714promptrecall';
|
||||
@@ -59,6 +59,41 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
let _contextHeaderSeq = 0;
|
||||
let _contextHeaderData = null;
|
||||
let _contextHeaderBound = false;
|
||||
let _pendingToolApproval = null;
|
||||
|
||||
function _submitToolApprovalWhenIdle(approvalId, label) {
|
||||
if (
|
||||
!_pendingToolApproval
|
||||
|| _pendingToolApproval.approval_id !== approvalId
|
||||
) return;
|
||||
if (isStreaming || _sendInFlight) {
|
||||
setTimeout(() => _submitToolApprovalWhenIdle(approvalId, label), 120);
|
||||
return;
|
||||
}
|
||||
const input = document.getElementById('message');
|
||||
if (input) {
|
||||
_pendingToolApproval.draft = input.value || '';
|
||||
input.value = label;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
const sendButton = document.querySelector('.send-btn');
|
||||
if (sendButton) sendButton.click();
|
||||
}
|
||||
|
||||
document.addEventListener('odysseus:tool-approval', (event) => {
|
||||
const detail = event && event.detail ? event.detail : {};
|
||||
const decision = String(detail.decision || '').toLowerCase();
|
||||
if (!detail.approval_id || !['approve', 'deny'].includes(decision)) return;
|
||||
_pendingToolApproval = {
|
||||
approval_id: String(detail.approval_id),
|
||||
decision,
|
||||
document_id: String(detail.document_id || ''),
|
||||
};
|
||||
_submitToolApprovalWhenIdle(
|
||||
_pendingToolApproval.approval_id,
|
||||
detail.label || (decision === 'approve' ? 'Allow once' : 'Deny'),
|
||||
);
|
||||
});
|
||||
|
||||
function _fmtContextNumber(n) {
|
||||
const v = Number(n || 0);
|
||||
@@ -1234,6 +1269,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
if (_sendInFlight) return;
|
||||
const _sendPerf = _createChatSendPerf();
|
||||
_sendInFlight = true;
|
||||
const approvalForSend = _pendingToolApproval;
|
||||
_setForegroundChatBusy(true);
|
||||
// Instant visual feedback so the user sees their click was accepted
|
||||
// even before the streaming button state kicks in below.
|
||||
@@ -1248,7 +1284,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
};
|
||||
|
||||
// --- Setup mode: intercept next message (but let slash commands through) ---
|
||||
{
|
||||
if (!approvalForSend) {
|
||||
const el = uiModule.el;
|
||||
const rawMsg = (el('message').value || '').trim();
|
||||
const currentSetupMode = slashCommands.getSetupMode();
|
||||
@@ -1278,7 +1314,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
if (!msg.trim() && !fileHandlerModule.getPendingCount() && !(_pendingRegenAttachments && _pendingRegenAttachments.length)) { _releaseSendFlag(); return; }
|
||||
|
||||
// --- Slash commands: execute directly without AI (no session needed) ---
|
||||
if (isCommand(msg.trim())) {
|
||||
if (!approvalForSend && isCommand(msg.trim())) {
|
||||
const handled = await handleSlashCommand(msg.trim());
|
||||
if (handled) {
|
||||
el('message').value = '';
|
||||
@@ -1405,7 +1441,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
}
|
||||
|
||||
// --- API key guard: warn if message looks like an API key ---
|
||||
if (API_KEY_RE.test(msg.trim())) {
|
||||
if (!approvalForSend && API_KEY_RE.test(msg.trim())) {
|
||||
if (!await window.styledConfirm('This looks like an API key. Sending it to the AI could expose it.\n\nDid you mean to use /setup instead?', { confirmText: 'Send anyway', danger: true })) {
|
||||
_releaseSendFlag();
|
||||
return;
|
||||
@@ -1540,7 +1576,9 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
if (sessionModule.clearStreamComplete) sessionModule.clearStreamComplete(sessionModule.getCurrentSessionId());
|
||||
|
||||
// Check for document selection context before consuming display override
|
||||
const docSel = documentModule && documentModule.getSelectionContext();
|
||||
const docSel = !approvalForSend && documentModule
|
||||
? documentModule.getSelectionContext()
|
||||
: null;
|
||||
if (docSel) {
|
||||
const sels = Array.isArray(docSel) ? docSel : [docSel];
|
||||
const lineRefs = sels.map(s =>
|
||||
@@ -1560,7 +1598,9 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
// stuck flag can't silently eat the next turn's recovery budget.
|
||||
if (!skipBubble) { _autoNudges = 0; _autoContinuePending = false; }
|
||||
else if (_autoContinuePending) { _autoContinuePending = false; }
|
||||
const _pendingAttachInfo = fileHandlerModule.getPendingCount() ? fileHandlerModule.getPendingInfo() : null;
|
||||
const _pendingAttachInfo = !approvalForSend && fileHandlerModule.getPendingCount()
|
||||
? fileHandlerModule.getPendingInfo()
|
||||
: null;
|
||||
// Pre-read importable file contents before upload clears pending files
|
||||
const IMPORTABLE_EXT = /\.(txt|py|js|ts|html|htm|css|md|json|csv|yml|yaml|sh|sql|rs|go|java|c|cpp|h|rb|php|xml|jsx|tsx|log|toml|ini|conf|env|vue|svelte|scss|sass|less)$/i;
|
||||
const _importableFiles = [];
|
||||
@@ -1578,7 +1618,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
_userMsgEl = addMessage('user', userDisplay, null, _pendingAttachInfo ? { attachments: _pendingAttachInfo } : null);
|
||||
}
|
||||
_sendPerf.mark('user_bubble_visible');
|
||||
messageInput.value = '';
|
||||
messageInput.value = approvalForSend ? (approvalForSend.draft || '') : '';
|
||||
messageInput.style.height = '';
|
||||
messageInput.dispatchEvent(new Event('input'));
|
||||
// Mobile: dismiss the on-screen keyboard after sending. iOS in
|
||||
@@ -1612,13 +1652,15 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
}
|
||||
|
||||
let ids = [];
|
||||
try {
|
||||
_sendPerf.mark('upload_begin');
|
||||
ids = await fileHandlerModule.uploadPending({ sessionId: sessionModule.getCurrentSessionId() });
|
||||
_sendPerf.mark('upload_done');
|
||||
} catch(e) {
|
||||
console.error('upload failed', e);
|
||||
_sendPerf.mark('upload_failed');
|
||||
if (!approvalForSend) {
|
||||
try {
|
||||
_sendPerf.mark('upload_begin');
|
||||
ids = await fileHandlerModule.uploadPending({ sessionId: sessionModule.getCurrentSessionId() });
|
||||
_sendPerf.mark('upload_done');
|
||||
} catch(e) {
|
||||
console.error('upload failed', e);
|
||||
_sendPerf.mark('upload_failed');
|
||||
}
|
||||
}
|
||||
if (_pendingAttachInfo && !ids.length && !(_pendingRegenAttachments && _pendingRegenAttachments.length)) {
|
||||
if (_userMsgEl && _userMsgEl.parentNode) _userMsgEl.remove();
|
||||
@@ -1635,10 +1677,10 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
// edited OCR text via the server-side .vision cache). Always CONSUME the
|
||||
// slot — even when empty / errored — so the regen ids can't bleed into
|
||||
// an unrelated next message if uploadPending() above had thrown.
|
||||
if (_pendingRegenAttachments && _pendingRegenAttachments.length) {
|
||||
if (!approvalForSend && _pendingRegenAttachments && _pendingRegenAttachments.length) {
|
||||
ids = ids.concat(_pendingRegenAttachments);
|
||||
}
|
||||
_pendingRegenAttachments = null;
|
||||
if (!approvalForSend) _pendingRegenAttachments = null;
|
||||
|
||||
// The optimistic user bubble was rendered before the upload assigned ids,
|
||||
// so image previews couldn't show (the renderer needs att.id). Now that
|
||||
@@ -1719,14 +1761,50 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
if (activeEmailComposerCtx?.docId) {
|
||||
activeDocIdForSend = activeEmailComposerCtx.docId;
|
||||
}
|
||||
if (documentModule && activeDocIdForSend) {
|
||||
const shouldSaveActiveDoc = !approvalForSend || (
|
||||
approvalForSend.document_id
|
||||
&& approvalForSend.document_id === activeDocIdForSend
|
||||
);
|
||||
if (documentModule && activeDocIdForSend && shouldSaveActiveDoc) {
|
||||
try {
|
||||
_sendPerf.mark('doc_save_begin');
|
||||
await documentModule.saveDocument();
|
||||
const documentSaved = await documentModule.saveDocument({
|
||||
silent: !!approvalForSend,
|
||||
});
|
||||
_sendPerf.mark('doc_save_done');
|
||||
if (approvalForSend && documentSaved === false) {
|
||||
if (_userMsgEl && _userMsgEl.parentNode) _userMsgEl.remove();
|
||||
if (
|
||||
_pendingToolApproval
|
||||
&& _pendingToolApproval.approval_id === approvalForSend.approval_id
|
||||
) {
|
||||
_pendingToolApproval = null;
|
||||
}
|
||||
uiModule.showError && uiModule.showError(
|
||||
'Document could not be saved, so the action was not approved. Reload the chat to retry.'
|
||||
);
|
||||
updateSubmitButton('idle', submitBtn);
|
||||
_releaseSendFlag();
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('doc auto-save failed', e);
|
||||
_sendPerf.mark('doc_save_failed');
|
||||
if (approvalForSend) {
|
||||
if (_userMsgEl && _userMsgEl.parentNode) _userMsgEl.remove();
|
||||
if (
|
||||
_pendingToolApproval
|
||||
&& _pendingToolApproval.approval_id === approvalForSend.approval_id
|
||||
) {
|
||||
_pendingToolApproval = null;
|
||||
}
|
||||
uiModule.showError && uiModule.showError(
|
||||
'Document could not be saved, so the action was not approved. Reload the chat to retry.'
|
||||
);
|
||||
updateSubmitButton('idle', submitBtn);
|
||||
_releaseSendFlag();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1756,18 +1834,30 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
const fd = new FormData();
|
||||
fd.append('message', _finalMsgWithInject);
|
||||
fd.append('session', streamSessionId);
|
||||
if (approvalForSend) {
|
||||
fd.append('tool_approval_id', approvalForSend.approval_id);
|
||||
fd.append('tool_approval_decision', approvalForSend.decision);
|
||||
if (
|
||||
_pendingToolApproval
|
||||
&& _pendingToolApproval.approval_id === approvalForSend.approval_id
|
||||
) {
|
||||
_pendingToolApproval = null;
|
||||
}
|
||||
}
|
||||
if (selectedRouteForSend.model) fd.append('selected_model', selectedRouteForSend.model);
|
||||
if (selectedRouteForSend.endpoint_url) fd.append('selected_endpoint_url', selectedRouteForSend.endpoint_url);
|
||||
if (selectedRouteForSend.endpoint_id) fd.append('selected_endpoint_id', selectedRouteForSend.endpoint_id);
|
||||
if (ids.length) fd.append('attachments', JSON.stringify(ids));
|
||||
// Auto-save & send active doc ID so the backend sees latest content
|
||||
if (documentModule && activeDocIdForSend) {
|
||||
try {
|
||||
_sendPerf.mark('doc_silent_save_begin');
|
||||
await documentModule.saveDocument({ silent: true });
|
||||
_sendPerf.mark('doc_silent_save_done');
|
||||
} catch (_e) {
|
||||
_sendPerf.mark('doc_silent_save_failed');
|
||||
if (documentModule && activeDocIdForSend && shouldSaveActiveDoc) {
|
||||
if (!approvalForSend) {
|
||||
try {
|
||||
_sendPerf.mark('doc_silent_save_begin');
|
||||
await documentModule.saveDocument({ silent: true });
|
||||
_sendPerf.mark('doc_silent_save_done');
|
||||
} catch (_e) {
|
||||
_sendPerf.mark('doc_silent_save_failed');
|
||||
}
|
||||
}
|
||||
fd.append('active_doc_id', activeDocIdForSend);
|
||||
}
|
||||
@@ -1821,7 +1911,7 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
if (isAgentMode) {
|
||||
fd.append('allow_web_search', el('web-toggle').checked ? 'true' : 'false');
|
||||
}
|
||||
if (el('research-toggle').checked) {
|
||||
if (!approvalForSend && el('research-toggle').checked) {
|
||||
fd.append('use_research', 'true');
|
||||
// Research always runs in chat mode — override agent if set
|
||||
fd.set('mode', 'chat');
|
||||
@@ -2152,9 +2242,6 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
_roundDisplayProjector.reset();
|
||||
_replyDisplayProjector.reset();
|
||||
_docFenceOpened = false;
|
||||
_docFenceContentStart = -1;
|
||||
_docFenceCandidateStart = -1;
|
||||
_docFenceCandidateMarker = '';
|
||||
}
|
||||
const esc = uiModule.esc;
|
||||
// Remove thinking spinner helper
|
||||
@@ -2244,9 +2331,6 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
|
||||
// Document streaming state (text-fence detection)
|
||||
let _docFenceOpened = false;
|
||||
let _docFenceContentStart = -1;
|
||||
let _docFenceCandidateStart = -1;
|
||||
let _docFenceCandidateMarker = '';
|
||||
const _thinkingAnalysisGate = createThinkingAnalysisGate({
|
||||
startsWithReasoningPrefix: markdownModule.startsWithReasoningPrefix,
|
||||
});
|
||||
@@ -2841,42 +2925,11 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
roundText += _delta;
|
||||
_roundDisplayProjector.append(_delta, roundText);
|
||||
|
||||
// --- Text-fence doc streaming (for models that don't use native tool calls) ---
|
||||
if (!_docFenceOpened && documentModule) {
|
||||
// Only inspect the newly appended boundary. Re-scanning the
|
||||
// full round for every reasoning delta is quadratic even
|
||||
// before thinking normalization runs.
|
||||
const fenceMarkers = ['```document\n', '```documen\n', '```create_document\n'];
|
||||
const fenceScanStart = Math.max(0, roundText.length - _delta.length - 24);
|
||||
if (_docFenceCandidateStart < 0) {
|
||||
for (const candidate of fenceMarkers) {
|
||||
const candidateIdx = roundText.indexOf(candidate, fenceScanStart);
|
||||
if (candidateIdx >= 0 && (_docFenceCandidateStart < 0 || candidateIdx < _docFenceCandidateStart)) {
|
||||
_docFenceCandidateMarker = candidate;
|
||||
_docFenceCandidateStart = candidateIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_docFenceCandidateStart >= 0) {
|
||||
const afterFence = roundText.slice(_docFenceCandidateStart + _docFenceCandidateMarker.length);
|
||||
const fenceLines = afterFence.split('\n');
|
||||
if (fenceLines.length >= 1 && fenceLines[0].trim()) {
|
||||
_docFenceOpened = true;
|
||||
const title = fenceLines[0].trim();
|
||||
// Keep in sync with backend _KNOWN_LANGS in src/tool_implementations.py
|
||||
const knownLangs = ['python','py','javascript','js','typescript','ts','html','css','json','yaml','bash','sql','rust','go','java','c','cpp','markdown','text','plain','ruby','swift','kotlin','php','email','csv','xml','toml','ini'];
|
||||
const isLang = fenceLines.length >= 2 && knownLangs.includes(fenceLines[1].trim().toLowerCase());
|
||||
const lang = isLang ? fenceLines[1].trim() : '';
|
||||
_docFenceContentStart = _docFenceCandidateStart + _docFenceCandidateMarker.length + title.length + 1 + (isLang ? fenceLines[1].length + 1 : 0);
|
||||
documentModule.streamDocOpen(title, lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_docFenceOpened && _docFenceContentStart > 0 && documentModule) {
|
||||
let raw = roundText.slice(_docFenceContentStart);
|
||||
const closeIdx = raw.indexOf('\n```');
|
||||
if (closeIdx >= 0) raw = raw.slice(0, closeIdx);
|
||||
documentModule.streamDocDelta(raw);
|
||||
// Raw model text is not authorization to mutate the editor.
|
||||
// Detect document fences only for chat projection/status; the
|
||||
// server emits doc_stream_* after successful dispatch.
|
||||
if (!_docFenceOpened) {
|
||||
_docFenceOpened = /```(?:create_document|documen(?:t)?)\s*\n/i.test(roundText);
|
||||
}
|
||||
|
||||
// Detect thinking-in-progress:
|
||||
@@ -3796,9 +3849,6 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea
|
||||
_roundDisplayProjector.reset();
|
||||
_replyDisplayProjector.reset();
|
||||
_docFenceOpened = false;
|
||||
_docFenceContentStart = -1;
|
||||
_docFenceCandidateStart = -1;
|
||||
_docFenceCandidateMarker = '';
|
||||
const box = document.getElementById('chat-history');
|
||||
const newWrap = document.createElement('div');
|
||||
newWrap.className = 'msg msg-ai msg-continuation streaming';
|
||||
|
||||
@@ -1367,7 +1367,7 @@ document.addEventListener('click', function(e) {
|
||||
} catch {}
|
||||
});
|
||||
} else if (kind === 'document') {
|
||||
import('./document.js?v=20260722emailfastindex1').then(mod => {
|
||||
import('./document.js?v=20260815approvalsave1').then(mod => {
|
||||
const open = mod.loadDocument
|
||||
|| mod.openDocument
|
||||
|| (mod.default && (mod.default.loadDocument || mod.default.openDocument));
|
||||
@@ -1389,7 +1389,7 @@ document.addEventListener('click', function(e) {
|
||||
if (open) open(id);
|
||||
}).catch(() => {});
|
||||
} else if (kind === 'email') {
|
||||
import('./emailLibrary.js?v=20260722emailfastindex1').then(mod => {
|
||||
import('./emailLibrary.js?v=20260815approvalsave1').then(mod => {
|
||||
const open = mod.openEmailLibrary || (mod.default && mod.default.openEmailLibrary);
|
||||
if (open) open({ uid: id });
|
||||
}).catch(() => {});
|
||||
@@ -2342,6 +2342,7 @@ export function renderAskUserCard(payload, options) {
|
||||
card.setAttribute('role', 'group');
|
||||
card.tabIndex = -1;
|
||||
const multi = !!aq.multi;
|
||||
const isToolApproval = aq.kind === 'tool_approval' && !!aq.approval_id;
|
||||
const emojiText = (value) => svgifyEmoji(uiModule.esc(String(value)));
|
||||
|
||||
const head = document.createElement('div');
|
||||
@@ -2366,6 +2367,27 @@ export function renderAskUserCard(payload, options) {
|
||||
card.appendChild(question);
|
||||
card.setAttribute('aria-labelledby', question.id);
|
||||
|
||||
if (isToolApproval && aq.action) {
|
||||
const action = document.createElement('div');
|
||||
action.className = 'ask-user-option-desc';
|
||||
const effects = Array.isArray(aq.action.effects)
|
||||
? aq.action.effects.join(', ')
|
||||
: '';
|
||||
action.textContent = [
|
||||
aq.action.tool || 'tool',
|
||||
aq.action.content || '',
|
||||
effects ? `Effects: ${effects}` : '',
|
||||
aq.action.workspace ? `Workspace: ${aq.action.workspace}` : '',
|
||||
aq.action.document_id ? `Document: ${aq.action.document_id}` : '',
|
||||
aq.action.document_version != null
|
||||
? `Document version: ${aq.action.document_version}`
|
||||
: '',
|
||||
aq.action.digest ? `Approval fingerprint: ${aq.action.digest}` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
action.style.whiteSpace = 'pre-wrap';
|
||||
card.appendChild(action);
|
||||
}
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'ask-user-options';
|
||||
card.appendChild(list);
|
||||
@@ -2403,7 +2425,23 @@ export function renderAskUserCard(payload, options) {
|
||||
}
|
||||
if (!multi) {
|
||||
row.type = 'button';
|
||||
row.addEventListener('click', () => send(label));
|
||||
row.addEventListener('click', () => {
|
||||
if (isToolApproval) {
|
||||
card.remove();
|
||||
document.dispatchEvent(new CustomEvent('odysseus:tool-approval', {
|
||||
detail: {
|
||||
approval_id: aq.approval_id,
|
||||
decision: String((opt && opt.value) || '').toLowerCase(),
|
||||
label,
|
||||
document_id: aq.action && aq.action.document_id
|
||||
? String(aq.action.document_id)
|
||||
: '',
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
send(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
list.appendChild(row);
|
||||
});
|
||||
@@ -2439,7 +2477,7 @@ export function renderAskUserCard(payload, options) {
|
||||
});
|
||||
other.appendChild(otherInput);
|
||||
other.appendChild(otherSend);
|
||||
card.appendChild(other);
|
||||
if (!isToolApproval) card.appendChild(other);
|
||||
|
||||
chatBox.appendChild(card);
|
||||
if (renderOptions.scroll !== false) {
|
||||
@@ -2489,7 +2527,7 @@ export function addMessage(role, content, modelName, metadata) {
|
||||
|
||||
const toolsByRound = {};
|
||||
for (const ev of toolEvents) {
|
||||
const r = ev.round || 1;
|
||||
const r = ev.round ?? 1;
|
||||
if (!toolsByRound[r]) toolsByRound[r] = [];
|
||||
toolsByRound[r].push(ev);
|
||||
}
|
||||
@@ -2497,9 +2535,12 @@ export function addMessage(role, content, modelName, metadata) {
|
||||
const toolRounds = Object.keys(toolsByRound).map(Number);
|
||||
const maxRound = Math.max(toolRounds.length ? Math.max(...toolRounds) : 0, roundTexts.length);
|
||||
|
||||
for (let r = 0; r < maxRound; r++) {
|
||||
const roundNum = r + 1;
|
||||
const txt = resolveDocumentPlaceholderLinks((roundTexts[r] || '').trim(), metadata);
|
||||
const firstRound = (toolsByRound[0] || []).length ? 0 : 1;
|
||||
for (let roundNum = firstRound; roundNum <= maxRound; roundNum++) {
|
||||
const r = roundNum - 1;
|
||||
const txt = r >= 0
|
||||
? resolveDocumentPlaceholderLinks((roundTexts[r] || '').trim(), metadata)
|
||||
: '';
|
||||
|
||||
if (txt) {
|
||||
const wrap = document.createElement('div');
|
||||
|
||||
@@ -7,7 +7,7 @@ import Storage from './storage.js';
|
||||
import themeModule from './theme.js';
|
||||
import markdownModule from './markdown.js';
|
||||
import sessionModule from './sessions.js';
|
||||
import documentModule from './document.js?v=20260722emailfastindex1';
|
||||
import documentModule from './document.js?v=20260815approvalsave1';
|
||||
|
||||
/**
|
||||
* Handle a ui_control SSE event — AI-driven UI manipulation.
|
||||
@@ -156,7 +156,7 @@ export function handleUIControl(uiData) {
|
||||
if (fn) fn();
|
||||
}).catch(function(){});
|
||||
} else if (panel === 'email') {
|
||||
import('./emailLibrary.js?v=20260722emailfastindex1').then(function(mod) {
|
||||
import('./emailLibrary.js?v=20260815approvalsave1').then(function(mod) {
|
||||
var fn = mod.openEmailLibrary || (mod.default && mod.default.openEmailLibrary);
|
||||
if (fn) fn();
|
||||
}).catch(function(){});
|
||||
@@ -205,7 +205,7 @@ export function handleUIControl(uiData) {
|
||||
} catch (e) {
|
||||
console.warn('open_email_reply existing draft update failed:', e);
|
||||
}
|
||||
import('./emailInbox.js?v=20260722emailfastindex1').then(function(mod) {
|
||||
import('./emailInbox.js?v=20260815approvalsave1').then(function(mod) {
|
||||
var fn = mod.openReplyDraft || (mod.default && mod.default.openReplyDraft);
|
||||
if (fn) fn(uiData.uid, uiData.folder || 'INBOX', uiData.mode || 'reply', uiData.body || '');
|
||||
}).catch(function(e) {
|
||||
|
||||
@@ -3934,7 +3934,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
leadingIcon: 'check',
|
||||
action: 'View Message',
|
||||
onAction: () => {
|
||||
import('./emailLibrary.js?v=20260722emailfastindex1').then(mod => {
|
||||
import('./emailLibrary.js?v=20260815approvalsave1').then(mod => {
|
||||
const open = mod.openEmailLibrary || (mod.default && mod.default.openEmailLibrary);
|
||||
if (open) open({
|
||||
account_id: data.account_id || activeAccountId || null,
|
||||
@@ -9401,9 +9401,9 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
|
||||
/** Save manual edits */
|
||||
export async function saveDocument({ silent = false, forceVersion = false } = {}) {
|
||||
if (!activeDocId) return;
|
||||
if (!activeDocId) return false;
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
if (!textarea) return;
|
||||
if (!textarea) return false;
|
||||
const savingDocId = activeDocId;
|
||||
saveCurrentToMap();
|
||||
const localDoc = docs.get(savingDocId);
|
||||
@@ -9422,7 +9422,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
});
|
||||
if (res.status === 404) {
|
||||
if (silent && localDoc?.language === 'email') {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// Streaming/empty email drafts can leave a local tab pointing at a temp
|
||||
// or already-deleted document. Do not keep surfacing autosave errors for
|
||||
@@ -9434,7 +9434,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
}
|
||||
_syncDocIndicator();
|
||||
if (!silent && uiModule) uiModule.showError('Document no longer exists');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Document save failed: HTTP ${res.status}`);
|
||||
const doc = await res.json();
|
||||
@@ -9447,6 +9447,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
}
|
||||
_syncDocIndicator();
|
||||
if (!silent && uiModule) uiModule.showToast(forceVersion ? 'New version saved' : 'Document saved');
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to save document:', e);
|
||||
const now = Date.now();
|
||||
@@ -9454,6 +9455,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
uiModule.showError(silent ? 'Autosave failed' : 'Failed to save document');
|
||||
_lastAutoSaveErrorAt = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import spinnerModule from './spinner.js';
|
||||
import sessionModule from './sessions.js';
|
||||
import { initEmailLibrary, openEmailLibrary, closeEmailLibrary, isOpen as isLibOpen, prewarmEmailLibrary, prewarmUnreadEmails } from './emailLibrary.js?v=20260722emailfastindex1';
|
||||
import { initEmailLibrary, openEmailLibrary, closeEmailLibrary, isOpen as isLibOpen, prewarmEmailLibrary, prewarmUnreadEmails } from './emailLibrary.js?v=20260815approvalsave1';
|
||||
import * as Modals from './modalManager.js';
|
||||
import { applyEdgeDock } from './modalSnap.js';
|
||||
import { buildReplyAllCc, extractEmail } from './emailLibrary/replyRecipients.js';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import spinnerModule from './spinner.js';
|
||||
import { styledConfirm, showToast, emptyStateIcon } from './ui.js';
|
||||
import { folderDisplayName, sortedFolders } from './emailInbox.js?v=20260722emailfastindex1';
|
||||
import { folderDisplayName, sortedFolders } from './emailInbox.js?v=20260815approvalsave1';
|
||||
import settingsModule from './settings.js';
|
||||
import * as Modals from './modalManager.js';
|
||||
import { topPortalZ } from './toolWindowZOrder.js';
|
||||
@@ -6680,7 +6680,7 @@ function _wireAttachmentHandlers(reader, folder) {
|
||||
ownerModal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
const docMod = await import('./document.js?v=20260722emailfastindex1');
|
||||
const docMod = await import('./document.js?v=20260815approvalsave1');
|
||||
const load = (docMod && docMod.loadDocument) || (docMod && docMod.default && docMod.default.loadDocument);
|
||||
if (typeof load === 'function') {
|
||||
await load(json.doc_id);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import Storage from './storage.js';
|
||||
import uiModule, { autoResize, styledPrompt } from './ui.js';
|
||||
import chatRenderer from './chatRenderer.js?v=20260722ctxheader1';
|
||||
import chatRenderer from './chatRenderer.js?v=20260815toolapproval4';
|
||||
import { providerLogo } from './providers.js';
|
||||
import { initModelPicker, updateModelPicker } from './modelPicker.js?v=20260722ctxheader1';
|
||||
import themeModule from './theme.js';
|
||||
|
||||
@@ -2745,7 +2745,7 @@ async function initEmailAccountsSettings() {
|
||||
|
||||
el('set-email-open-library-settings')?.addEventListener('click', async () => {
|
||||
try {
|
||||
const mod = await import('./emailLibrary.js?v=20260722emailfastindex1');
|
||||
const mod = await import('./emailLibrary.js?v=20260815approvalsave1');
|
||||
if (typeof mod.openEmailLibrarySettings === 'function') {
|
||||
await mod.openEmailLibrarySettings();
|
||||
}
|
||||
|
||||
@@ -1128,10 +1128,67 @@ function _renderTestLog(logEl, verdictEl, job, card, name) {
|
||||
else if (ev.type === 'agent_step') add('— round ' + ev.round + ' —', 'skill-test-round');
|
||||
else if (ev.type === 'tool_start') add('▸ ' + ev.tool + ' ' + String(ev.command || '').slice(0, 200), 'skill-test-tool');
|
||||
else if (ev.type === 'tool_output') add(String(ev.output || '').slice(0, 500), 'skill-test-out');
|
||||
else if (ev.type === 'approval_granted' || ev.type === 'approval_denied') add(ev.text || '', 'skill-test-meta');
|
||||
else if (ev.type === 'say') add(ev.text || '', 'skill-test-say');
|
||||
else if (ev.type === 'evaluating') add('Evaluating run…', 'skill-test-meta');
|
||||
else if (ev.type === 'error') add('Error: ' + (ev.error || 'run failed'), 'skill-test-err');
|
||||
}
|
||||
if (job.status === 'awaiting_approval' && job.approval) {
|
||||
const approval = job.approval;
|
||||
const box = document.createElement('div');
|
||||
box.className = 'skill-test-approval';
|
||||
const question = document.createElement('div');
|
||||
question.className = 'skill-test-meta';
|
||||
question.textContent = approval.question || 'Allow this exact action once?';
|
||||
box.appendChild(question);
|
||||
if (approval.action) {
|
||||
const action = document.createElement('pre');
|
||||
action.className = 'skill-test-out';
|
||||
action.textContent = [
|
||||
approval.action.tool || 'tool',
|
||||
approval.action.content || '',
|
||||
Array.isArray(approval.action.effects)
|
||||
? `Effects: ${approval.action.effects.join(', ')}`
|
||||
: '',
|
||||
approval.action.workspace ? `Workspace: ${approval.action.workspace}` : '',
|
||||
approval.action.digest ? `Approval fingerprint: ${approval.action.digest}` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
box.appendChild(action);
|
||||
}
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'modal-footer';
|
||||
const decide = async (decision) => {
|
||||
actions.querySelectorAll('button').forEach(btn => { btn.disabled = true; });
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API}/api/skills/${encodeURIComponent(name)}/test-approval`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ approval_id: approval.approval_id, decision }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
await _testSkill(card, name, false);
|
||||
} catch (error) {
|
||||
add(`Approval failed: ${error.message || error}`, 'skill-test-err');
|
||||
actions.querySelectorAll('button').forEach(btn => { btn.disabled = false; });
|
||||
}
|
||||
};
|
||||
for (const [decision, label, cls] of [
|
||||
['deny', 'Deny', 'confirm-btn confirm-btn-secondary'],
|
||||
['approve', 'Allow once', 'confirm-btn confirm-btn-primary'],
|
||||
]) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = cls;
|
||||
button.textContent = label;
|
||||
button.addEventListener('click', () => decide(decision));
|
||||
actions.appendChild(button);
|
||||
}
|
||||
box.appendChild(actions);
|
||||
logEl.appendChild(box);
|
||||
}
|
||||
if (job.status === 'running') add('…running (you can close this — it keeps going)', 'skill-test-meta');
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
if (job.status === 'done' && job.verdict) _renderTestVerdict(verdictEl, job.verdict, card, name);
|
||||
|
||||
@@ -16,7 +16,7 @@ import modelsModule from './models.js';
|
||||
import chatRenderer from './chatRenderer.js';
|
||||
import spinnerModule from './spinner.js';
|
||||
import themeModule from './theme.js';
|
||||
import documentModule from './document.js?v=20260722emailfastindex1';
|
||||
import documentModule from './document.js?v=20260815approvalsave1';
|
||||
import workspaceModule from './workspace.js';
|
||||
import settingsModule from './settings.js';
|
||||
import cookbookModule from './cookbook.js';
|
||||
|
||||
Reference in New Issue
Block a user