- Neues Tool send_to_session: Agent kann Nachrichten in andere Sessions schicken - Ziel-Session per ID oder Name (case-insensitive) auffindbar - User-Message wird nicht in DB gespeichert (wie Cronjob) – keine Bubble im Frontend - Nachricht wird als '[Nachricht von Agent "X"]' kontextualisiert injiziert - Sicherheitscheck: Ziel-Session muss demselben User gehören, keine eigene Session - Zirkuläre Abhängigkeit ChatService ↔ AnthropicService via SetChatService() gelöst - tool-ui.js: Display-Name für send_to_session
263 lines
8.4 KiB
JavaScript
263 lines
8.4 KiB
JavaScript
// ── Tool Call Visibility ──
|
||
|
||
const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write', 'remote_execute'];
|
||
const TOOL_DISPLAY_NAMES = {
|
||
write_file: 'Datei schreiben',
|
||
edit_file: 'Datei bearbeiten',
|
||
delete_file: 'Datei löschen',
|
||
memory_write: 'Erinnerungen festhalten',
|
||
web_search: 'Web-Suche',
|
||
remote_execute: 'Remote-Befehl',
|
||
memory_search: 'Erinnerungen durchsuchen',
|
||
send_to_session: 'Nachricht an Session senden'
|
||
};
|
||
|
||
function isToolVisible(toolName) {
|
||
const mode = loadSettings().toolCallDisplay;
|
||
if (mode === 'all') return true;
|
||
if (mode === 'hidden') return false;
|
||
// 'modifications' – nur modifizierende Tools + web_search anzeigen
|
||
if (toolName === 'web_search') return true;
|
||
return MODIFICATION_TOOLS.includes(toolName);
|
||
}
|
||
|
||
// ── Chain Meta Visibility ──
|
||
|
||
function isChainMetaVisible(chainData) {
|
||
const mode = loadSettings().chainMetaDisplay;
|
||
if (mode === 'always') return true;
|
||
if (mode === 'never') return false;
|
||
// 'sometimes' – nur bei auffälligen Chains anzeigen
|
||
if (!chainData) return false;
|
||
const inOut = (chainData.inputTokens || 0) + (chainData.outputTokens || 0);
|
||
if (inOut >= 10000) return true;
|
||
if ((chainData.iterations || 0) >= 25) return true;
|
||
if ((chainData.durationMs || 0) >= 90000) return true;
|
||
return false;
|
||
}
|
||
|
||
// ── Tool Boxes ──
|
||
|
||
function addToolBoxLoading(name) {
|
||
const div = document.createElement('div');
|
||
div.className = 'tool-box streamed';
|
||
div.dataset.toolName = name;
|
||
const displayName = TOOL_DISPLAY_NAMES[name] || name;
|
||
div.innerHTML = '<span class="tool-name">' + esc(displayName) + '</span> <span class="tool-spinner"><i class="fa fa-hourglass-half"></i></span>';
|
||
chatEl().appendChild(div);
|
||
scrollDown();
|
||
return div;
|
||
}
|
||
|
||
function renderToolBoxContent(div, name, inputJson, result, isError, sources) {
|
||
let parsedInput = null;
|
||
try { parsedInput = JSON.parse(inputJson); } catch {}
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'tool-box-header';
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.className = 'tool-name';
|
||
nameSpan.textContent = TOOL_DISPLAY_NAMES[name] || name;
|
||
header.appendChild(nameSpan);
|
||
|
||
if (isError) {
|
||
const errSpan = document.createElement('span');
|
||
errSpan.className = 'tool-error-badge';
|
||
errSpan.textContent = 'error';
|
||
header.appendChild(errSpan);
|
||
}
|
||
|
||
div.innerHTML = '';
|
||
div.appendChild(header);
|
||
|
||
let foldTarget = null;
|
||
|
||
// Web-Search Sources – immer sichtbar, kein Toggle
|
||
if (name === 'web_search' && sources && sources.length > 0) {
|
||
const sourcesList = document.createElement('div');
|
||
sourcesList.className = 'web-search-sources';
|
||
sources.forEach(function(s) {
|
||
const link = document.createElement('a');
|
||
link.href = s.url;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
link.className = 'web-search-source';
|
||
var domain = '';
|
||
try { domain = new URL(s.url).hostname.replace('www.', ''); } catch(e) { domain = s.url; }
|
||
link.innerHTML = '<i class="fa fa-external-link"></i> <span class="source-domain">' + esc(domain) + '</span>' +
|
||
(s.title ? '<span class="source-title">' + esc(s.title) + '</span>' : '');
|
||
sourcesList.appendChild(link);
|
||
});
|
||
div.appendChild(sourcesList);
|
||
}
|
||
|
||
if (name === 'edit_file' && parsedInput && parsedInput.path) {
|
||
const pathSpan = document.createElement('span');
|
||
pathSpan.className = 'tool-input';
|
||
pathSpan.textContent = parsedInput.path;
|
||
header.appendChild(pathSpan);
|
||
|
||
const diff = buildEditDiff(parsedInput.old_text || '', parsedInput.new_text || '');
|
||
diff.hidden = true;
|
||
div.appendChild(diff);
|
||
foldTarget = diff;
|
||
|
||
} else if (name === 'write_file' && parsedInput && parsedInput.path) {
|
||
const pathSpan = document.createElement('span');
|
||
pathSpan.className = 'tool-input';
|
||
pathSpan.textContent = parsedInput.path;
|
||
header.appendChild(pathSpan);
|
||
|
||
const content = parsedInput.content || '';
|
||
const lines = content.split('\n');
|
||
const lineCount = lines.length;
|
||
const charCount = content.length;
|
||
|
||
const meta = document.createElement('span');
|
||
meta.className = 'tool-write-meta';
|
||
meta.textContent = lineCount + ' Zeilen, ' + charCount + ' Zeichen';
|
||
header.appendChild(meta);
|
||
|
||
const preview = buildWritePreview(content);
|
||
preview.hidden = true;
|
||
div.appendChild(preview);
|
||
foldTarget = preview;
|
||
|
||
} else if (name === 'todo_write') {
|
||
// Todo-Liste: kein tool-input, standardmäßig sichtbar, custom parser
|
||
if (result) {
|
||
const todoList = buildTodoList(result, isError);
|
||
div.appendChild(todoList);
|
||
foldTarget = todoList;
|
||
}
|
||
|
||
} else {
|
||
// Standard: path/command als input anzeigen
|
||
let inputLabel = '';
|
||
if (parsedInput) {
|
||
inputLabel = parsedInput.path || parsedInput.command || JSON.stringify(parsedInput).substring(0, 120);
|
||
} else {
|
||
inputLabel = truncate(inputJson, 120);
|
||
}
|
||
if (inputLabel) {
|
||
const inputSpan = document.createElement('span');
|
||
inputSpan.className = 'tool-input';
|
||
inputSpan.textContent = inputLabel;
|
||
header.appendChild(inputSpan);
|
||
}
|
||
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
|
||
resultDiv.textContent = result || '';
|
||
resultDiv.hidden = true;
|
||
div.appendChild(resultDiv);
|
||
foldTarget = resultDiv;
|
||
}
|
||
|
||
if (foldTarget && name !== 'todo_write') {
|
||
header.style.cursor = 'pointer';
|
||
header.addEventListener('click', () => {
|
||
foldTarget.hidden = !foldTarget.hidden;
|
||
});
|
||
}
|
||
}
|
||
|
||
function _buildDiffLines(container, lines, type) {
|
||
const cssClass = type === 'added' ? 'diff-added' : 'diff-removed';
|
||
const markerChar = type === 'added' ? '+' : '-';
|
||
for (const line of lines) {
|
||
const row = document.createElement('div');
|
||
row.className = 'diff-line ' + cssClass;
|
||
const marker = document.createElement('span');
|
||
marker.className = 'diff-marker';
|
||
marker.textContent = markerChar;
|
||
const content = document.createElement('span');
|
||
content.className = 'diff-content';
|
||
content.textContent = line;
|
||
row.appendChild(marker);
|
||
row.appendChild(content);
|
||
container.appendChild(row);
|
||
}
|
||
}
|
||
|
||
function buildEditDiff(oldText, newText) {
|
||
const container = document.createElement('div');
|
||
container.className = 'tool-diff';
|
||
_buildDiffLines(container, oldText.split('\n'), 'removed');
|
||
_buildDiffLines(container, newText.split('\n'), 'added');
|
||
return container;
|
||
}
|
||
|
||
function buildWritePreview(content) {
|
||
const container = document.createElement('div');
|
||
container.className = 'tool-diff';
|
||
_buildDiffLines(container, content.split('\n'), 'added');
|
||
return container;
|
||
}
|
||
|
||
function buildTodoList(result, isError) {
|
||
const container = document.createElement('div');
|
||
container.className = 'todo-list' + (isError ? ' tool-error' : '');
|
||
|
||
const lines = result.split('\n').map(l => l.replace(/\r$/, '')).filter(l => l.trim());
|
||
for (const line of lines) {
|
||
// Format: ICON [id] [priority] content
|
||
const m = line.match(/^(\S+)\s+\[([^\]]+)\]\s+(?:\[([^\]]+)\]\s+)?(.+)$/);
|
||
const row = document.createElement('div');
|
||
row.className = 'todo-item';
|
||
|
||
if (m) {
|
||
const icon = m[1]; // ✅ / 🔄 / ⬜
|
||
const id = m[2];
|
||
const prio = m[3] || null;
|
||
const text = m[4];
|
||
|
||
const statusClass =
|
||
icon === '✅' ? 'todo-done' :
|
||
icon === '🔄' ? 'todo-progress' :
|
||
'todo-pending';
|
||
row.classList.add(statusClass);
|
||
|
||
const iconSpan = document.createElement('span');
|
||
iconSpan.className = 'todo-icon';
|
||
iconSpan.textContent = icon;
|
||
|
||
const idSpan = document.createElement('span');
|
||
idSpan.className = 'todo-id';
|
||
idSpan.textContent = id + '.';
|
||
|
||
const textSpan = document.createElement('span');
|
||
textSpan.className = 'todo-text';
|
||
textSpan.textContent = text;
|
||
|
||
row.appendChild(iconSpan);
|
||
row.appendChild(idSpan);
|
||
row.appendChild(textSpan);
|
||
|
||
if (prio) {
|
||
const prioSpan = document.createElement('span');
|
||
prioSpan.className = 'todo-prio todo-prio-' + prio.toLowerCase();
|
||
prioSpan.textContent = prio;
|
||
row.appendChild(prioSpan);
|
||
}
|
||
} else {
|
||
row.textContent = line;
|
||
}
|
||
|
||
container.appendChild(row);
|
||
}
|
||
|
||
parseFluentEmojis(container);
|
||
|
||
return container;
|
||
}
|
||
|
||
function finishToolBox(div, data) {
|
||
if (!div) return;
|
||
renderToolBoxContent(div, data.name, data.input, data.result, data.isError, data.sources);
|
||
scrollDown();
|
||
}
|
||
|
||
|