feat: CLI-Streaming – korrekte Tool-Darstellung, Reihenfolge und Error-Handling

- Tool-Namen/Inputs von CLI-Konvention auf gott-Konvention mappen (Edit→edit_file, file_path→path etc.)
- Text vor Tool-Use in DB flushen für korrekte Reihenfolge (statt alles am Ende als Blob)
- lineBuffer in ClaudeCliService vor Tool-Start und bei Text-Block-Ende flushen
- ToolResult-Events parsen und in DB speichern (CLI sendet diese als type:"user" Messages)
- Errors in DB speichern und als SSE-Event senden
- Bild-Attachments als Temp-Dateien für CLI bereitstellen (Read-Tool kann sie lesen)
- Frontend: tool_result Handler, Tool-Name-Normalisierung, appendToolResult()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-18 06:48:10 +01:00
1 parent 3e372b5a6e
commit 6d16d00290
4 files changed
+417 -17

No files matched your search

+9
View File
@@ -52,10 +52,18 @@ function handleStreamEvent(eventType, data) {
case 'tool_end':
if (_currentToolBox) {
finishToolBox(_currentToolBox, data);
_lastFinishedToolBox = _currentToolBox;
}
_currentToolBox = null;
break;
case 'tool_result':
if (_lastFinishedToolBox && data.result) {
appendToolResult(_lastFinishedToolBox, data.result, data.isError);
}
_lastFinishedToolBox = null;
break;
case 'web_search_start':
pauseStreamBubble();
_currentToolBox = addToolBoxLoading('web_search');
@@ -158,6 +166,7 @@ function togglePlanMode() {
let isStreaming = false;
let _lastDoneChainData = null; // Chain-Daten vom letzten done-Event
let _currentToolBox = null; // Aktuell laufende Tool-Box (für Stream-Events)
let _lastFinishedToolBox = null; // Letzte fertige Tool-Box (für tool_result Zuordnung)
let _chatAbandoned = false; // true wenn User weg-navigiert während Stream läuft
let _abortController = null; // AbortController für laufenden Stream
+73 -6
View File
@@ -1,3 +1,50 @@
// ── 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',
'ToolSearch': 'tool_search'
};
/** Normalize CLI tool name to gott convention (Edit → edit_file, etc.) */
function normalizeToolName(name) {
return CLI_TOOL_NAME_MAP[name] || 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'];
@@ -9,16 +56,18 @@ const TOOL_DISPLAY_NAMES = {
web_search: 'Web-Suche',
remote_execute: 'Remote-Befehl',
memory_search: 'Erinnerungen durchsuchen',
send_to_session: 'Nachricht an Session senden'
send_to_session: 'Nachricht an Session senden',
tool_search: 'Tool-Suche'
};
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 (toolName === 'web_search') return true;
return MODIFICATION_TOOLS.includes(toolName);
if (normalized === 'web_search') return true;
return MODIFICATION_TOOLS.includes(normalized);
}
// ── Chain Meta Visibility ──
@@ -38,7 +87,8 @@ function isChainMetaVisible(chainData) {
// ── Tool Boxes ──
function addToolBoxLoading(name) {
function addToolBoxLoading(rawName) {
const name = normalizeToolName(rawName);
const div = document.createElement('div');
div.className = 'tool-box streamed';
div.dataset.toolName = name;
@@ -49,9 +99,10 @@ function addToolBoxLoading(name) {
return div;
}
function renderToolBoxContent(div, name, inputJson, result, isError, sources) {
function renderToolBoxContent(div, rawName, inputJson, result, isError, sources) {
const name = normalizeToolName(rawName);
let parsedInput = null;
try { parsedInput = JSON.parse(inputJson); } catch {}
try { parsedInput = normalizeToolInput(rawName, JSON.parse(inputJson)); } catch {}
const header = document.createElement('div');
header.className = 'tool-box-header';
@@ -259,4 +310,20 @@ function finishToolBox(div, data) {
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;
// 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
var toolName = div.dataset.toolName;
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);
}