Files
gott/wwwroot/js/tool-ui.js
T
MelekiandClaude Opus 4.6 9c507e4716 fix: TodoWrite-Tool über CLI korrekt als Todo-Liste rendern
TodoWrite → todo_write Mapping in Backend und Frontend ergänzt.
Todo-Liste wird jetzt aus dem Input-Array gebaut statt aus dem
Result, da die CLI nur einen generischen Erfolgstext zurückgibt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 07:07:12 +01:00

420 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── CLI Tool Name / Input Normalization ──
const CLI_TOOL_NAME_MAP = {
'Edit': 'edit_file',
'Write': 'write_file',
'Read': 'read_file',
'Bash': 'execute',
'Glob': 'list_files',
'Grep': 'search_files',
'WebSearch': 'web_search',
'WebFetch': 'web_fetch',
'NotebookEdit': 'notebook_edit',
'TodoWrite': 'todo_write',
'ToolSearch': 'tool_search',
'mcp__gott-remote__remote_execute': 'remote_execute'
};
/** Normalize CLI tool name to gott convention (Edit → edit_file, etc.) */
function normalizeToolName(name) {
if (CLI_TOOL_NAME_MAP[name]) return CLI_TOOL_NAME_MAP[name];
// Generisch: mcp__server__tool → tool
if (name && name.startsWith('mcp__')) {
var parts = name.split('__');
return parts[parts.length - 1] || name;
}
return name;
}
/** Normalize CLI tool input fields (file_path → path, old_string → old_text, etc.) */
function normalizeToolInput(name, parsedInput) {
if (!parsedInput || typeof parsedInput !== 'object') return parsedInput;
var normalized = name === 'edit_file' || name === 'Edit';
var isWrite = name === 'write_file' || name === 'Write';
var isRead = name === 'read_file' || name === 'Read';
var isBash = name === 'execute' || name === 'Bash';
if (normalized) {
return {
path: parsedInput.path || parsedInput.file_path,
old_text: parsedInput.old_text || parsedInput.old_string,
new_text: parsedInput.new_text || parsedInput.new_string
};
}
if (isWrite || isRead) {
var result = Object.assign({}, parsedInput);
if (parsedInput.file_path && !parsedInput.path) result.path = parsedInput.file_path;
return result;
}
if (isBash && parsedInput.command === undefined && parsedInput.description) {
// CLI Bash tool hat command direkt
return parsedInput;
}
return parsedInput;
}
// ── Tool Call Visibility ──
const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write', 'remote_execute'];
const TOOL_DISPLAY_NAMES = {
list_files: 'Dateien auflisten',
search_files: 'Dateien durchsuchen',
read_file: 'Datei lesen',
write_file: 'Datei schreiben',
edit_file: 'Datei bearbeiten',
delete_file: 'Datei löschen',
glob: 'Dateien durchsuchen',
grep: 'Dateien durchsuchen (Volltext)',
memory_search: 'Erinnerungen durchsuchen',
memory_read: 'Erinnerungen lesen',
memory_write: 'Erinnerungen festhalten',
web_fetch: 'Web-Fetch durchführen',
web_search: 'Web durchsuchen',
execute: 'Befehl ausführen',
background_execute: 'Befehl ausführen (Hintergrund)',
remote_execute: 'Befehl ausführen (Remote)',
send_to_session: 'Nachricht an Session senden',
tool_search: 'Tools durchsuchen',
todo_write: 'Aufgabenliste aktualisieren',
github_code_search: 'Code auf Github suchen',
serper_web_search: 'Web per Serper durchsuchen',
};
function isToolVisible(toolName) {
const normalized = normalizeToolName(toolName);
const mode = loadSettings().toolCallDisplay;
if (mode === 'all') return true;
if (mode === 'hidden') return false;
// 'modifications' – nur modifizierende Tools + web_search anzeigen
if (normalized === 'web_search') return true;
return MODIFICATION_TOOLS.includes(normalized);
}
// ── 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(rawName) {
const name = normalizeToolName(rawName);
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, rawName, inputJson, result, isError, sources) {
const name = normalizeToolName(rawName);
let parsedInput = null;
try { parsedInput = normalizeToolInput(rawName, 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: aus Input bauen (CLI hat nur generisches Result, API formatiert Result)
// Priorität: parsedInput.todos (immer vorhanden) > result (nur bei API-Pfad formatiert)
if (parsedInput && parsedInput.todos && Array.isArray(parsedInput.todos)) {
const todoList = buildTodoListFromInput(parsedInput.todos, isError);
div.appendChild(todoList);
foldTarget = todoList;
} else 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);
}
if (result) {
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.classList.add('clickable');
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;
}
/** Baut Todo-Liste direkt aus dem Input-Array (für CLI-Pfad, wo Result nur generischer Text ist). */
function buildTodoListFromInput(todos, isError) {
const container = document.createElement('div');
container.className = 'todo-list' + (isError ? ' tool-error' : '');
for (var i = 0; i < todos.length; i++) {
const todo = todos[i];
const row = document.createElement('div');
row.className = 'todo-item';
const status = todo.status || 'pending';
const icon = status === 'completed' ? '✅' : status === 'in_progress' ? '🔄' : '⬜';
const statusClass = status === 'completed' ? 'todo-done' : status === 'in_progress' ? '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 = (todo.id || (i + 1)) + '.';
const textSpan = document.createElement('span');
textSpan.className = 'todo-text';
// CLI hat "activeForm" statt nur "content" – zeige content (Imperativ-Form)
textSpan.textContent = todo.content || todo.activeForm || '';
row.appendChild(iconSpan);
row.appendChild(idSpan);
row.appendChild(textSpan);
if (todo.priority) {
const prioSpan = document.createElement('span');
prioSpan.className = 'todo-prio todo-prio-' + todo.priority.toLowerCase();
prioSpan.textContent = todo.priority;
row.appendChild(prioSpan);
}
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();
}
/** Fügt ein Tool-Result nachträglich an eine bestehende Tool-Box an (CLI tool_result Events). */
function appendToolResult(div, result, isError) {
if (!div || !result) return;
var toolName = div.dataset.toolName;
// todo_write: Liste wurde bereits aus Input gebaut (in renderToolBoxContent)
if (toolName === 'todo_write') {
if (div.querySelector('.todo-list')) return; // Bereits vorhanden
// Fallback: versuche aus Result zu parsen (API-Pfad)
var todoList = buildTodoList(result, isError);
div.appendChild(todoList);
scrollDown();
return;
}
// Nur anfügen wenn noch kein Result vorhanden
if (div.querySelector('.tool-result')) return;
// Bei edit_file/write_file: Diff reicht, kein extra Result nötig
if (toolName === 'edit_file' || toolName === 'write_file') return;
var rd = document.createElement('div');
rd.className = 'tool-result' + (isError ? ' tool-error' : '');
rd.textContent = truncate(result, 300);
rd.hidden = true;
div.appendChild(rd);
// Header clickable machen falls noch nicht
var hdr = div.querySelector('.tool-box-header');
if (hdr && !hdr.classList.contains('clickable')) {
hdr.classList.add('clickable');
hdr.addEventListener('click', function() { rd.hidden = !rd.hidden; });
}
}