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>
420 lines
14 KiB
JavaScript
420 lines
14 KiB
JavaScript
// ── 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; });
|
||
}
|
||
}
|
||
|
||
|