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:
1 parent
3e372b5a6e
commit
6d16d00290
4 files changed
+417
-17
No files matched your search
@@ -1656,6 +1656,83 @@ public class AnthropicService
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extrahiert Bild-Blöcke aus der letzten User-Message, speichert sie als Temp-Dateien
|
||||
/// und gibt Datei-Referenz-Hinweise zurück, die der CLI-Prompt vorangestellt werden.
|
||||
/// </summary>
|
||||
private static string ExtractLastUserImageRefs(List<object> messages, List<string> tempPaths)
|
||||
{
|
||||
for (int i = messages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(messages[i]);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
if (!el.TryGetProperty("role", out var role) || role.GetString() != "user") continue;
|
||||
if (!el.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) break;
|
||||
|
||||
var refs = new List<string>();
|
||||
foreach (var block in content.EnumerateArray())
|
||||
{
|
||||
var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() : "";
|
||||
if (blockType != "image" && blockType != "document") continue;
|
||||
|
||||
if (!block.TryGetProperty("source", out var source)) continue;
|
||||
var mediaType = source.TryGetProperty("media_type", out var mt) ? mt.GetString() ?? "" : "";
|
||||
var data = source.TryGetProperty("data", out var d) ? d.GetString() ?? "" : "";
|
||||
if (string.IsNullOrEmpty(data)) continue;
|
||||
|
||||
// Dateiendung aus media_type ableiten
|
||||
var ext = mediaType switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" => ".jpg",
|
||||
"image/gif" => ".gif",
|
||||
"image/webp" => ".webp",
|
||||
"application/pdf" => ".pdf",
|
||||
_ => ".bin"
|
||||
};
|
||||
|
||||
// Temp-Verzeichnis sicherstellen
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "gott-attachments");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
var fileName = $"{Guid.NewGuid():N}{ext}";
|
||||
var filePath = Path.Combine(tempDir, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(filePath, Convert.FromBase64String(data));
|
||||
tempPaths.Add(filePath);
|
||||
|
||||
var label = blockType == "image" ? "Screenshot" : "Datei";
|
||||
refs.Add($"[{label}: {filePath}]");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Base64-Decode fehlgeschlagen → überspringen
|
||||
}
|
||||
}
|
||||
|
||||
if (refs.Count > 0)
|
||||
{
|
||||
return string.Join(" ", refs) + "\n<hint>Lies die referenzierten Dateien mit dem Read-Tool um sie zu sehen.</hint>";
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Löscht temporäre Bilddateien die für den CLI-Aufruf erstellt wurden.
|
||||
/// </summary>
|
||||
private static void CleanupTempImages(List<string> paths)
|
||||
{
|
||||
if (paths == null || paths.Count == 0) return;
|
||||
foreach (var path in paths)
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
|
||||
/// </summary>
|
||||
@@ -1821,6 +1898,7 @@ public class AnthropicService
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var usage = new TokenUsage();
|
||||
var fullText = new StringBuilder();
|
||||
var tempImagePaths = new List<string>();
|
||||
|
||||
var chain = new ChainRecord
|
||||
{
|
||||
@@ -1861,6 +1939,14 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
||||
// Letzte User-Message als Prompt extrahieren
|
||||
var prompt = ExtractLastUserText(messages) ?? "";
|
||||
|
||||
// Bild-Attachments aus letzter User-Message als Temp-Dateien speichern
|
||||
// (CLI kann Bilder nicht inline empfangen, aber per Read-Tool lesen)
|
||||
var imageRefs = ExtractLastUserImageRefs(messages, tempImagePaths);
|
||||
if (!string.IsNullOrEmpty(imageRefs))
|
||||
{
|
||||
prompt = imageRefs + "\n" + prompt;
|
||||
}
|
||||
|
||||
// Konversations-History als Kontext zum Prompt hinzufügen
|
||||
var historyContext = BuildHistoryContext(messages);
|
||||
if (!string.IsNullOrEmpty(historyContext))
|
||||
@@ -1888,9 +1974,28 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
||||
await SseHelper.TryWriteSse(response, "text_end", new { }, cancellationToken);
|
||||
break;
|
||||
case CliEventType.ToolStart:
|
||||
await SseHelper.TryWriteSse(response, "tool_start", new { name = evt.ToolName, id = evt.ToolId }, cancellationToken);
|
||||
{
|
||||
// Text-Puffer flushen bevor Tool-Use gespeichert wird (korrekte Reihenfolge in DB)
|
||||
if (fullText.Length > 0)
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Iteration = 1,
|
||||
Role = "assistant",
|
||||
Type = "text",
|
||||
Content = fullText.ToString()
|
||||
});
|
||||
fullText.Clear();
|
||||
}
|
||||
var mappedName = MapCliToolName(evt.ToolName);
|
||||
await SseHelper.TryWriteSse(response, "tool_start", new { name = mappedName, id = evt.ToolId }, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case CliEventType.ToolEnd:
|
||||
{
|
||||
var mappedName = MapCliToolName(evt.ToolName);
|
||||
var mappedInput = MapCliToolInput(evt.ToolName, evt.ToolInput);
|
||||
// Tool-Use in DB speichern (damit Poll die Tools anzeigt)
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
@@ -1898,12 +2003,36 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
||||
Iteration = 1,
|
||||
Role = "assistant",
|
||||
Type = "tool_use",
|
||||
ToolName = evt.ToolName,
|
||||
ToolName = mappedName,
|
||||
ToolCallId = evt.ToolId,
|
||||
Content = evt.ToolInput
|
||||
Content = mappedInput
|
||||
});
|
||||
await SseHelper.TryWriteSse(response, "tool_end", new { name = evt.ToolName, id = evt.ToolId, input = evt.ToolInput }, cancellationToken);
|
||||
await SseHelper.TryWriteSse(response, "tool_end", new { name = mappedName, id = evt.ToolId, input = mappedInput }, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case CliEventType.ToolResult:
|
||||
{
|
||||
// Tool-Result in DB speichern (damit es beim Neuladen sichtbar ist)
|
||||
var resultContent = SseHelper.TruncateToolResult(evt.ToolResultContent ?? "");
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Iteration = 1,
|
||||
Role = "user",
|
||||
Type = "tool_result",
|
||||
ToolCallId = evt.ToolId,
|
||||
Content = resultContent,
|
||||
IsError = evt.ToolResultIsError
|
||||
});
|
||||
// SSE Event an Client senden
|
||||
await SseHelper.TryWriteSse(response, "tool_result", new
|
||||
{
|
||||
id = evt.ToolId,
|
||||
result = resultContent,
|
||||
isError = evt.ToolResultIsError
|
||||
}, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case CliEventType.UserChoice:
|
||||
// Text bisher in DB speichern
|
||||
if (fullText.Length > 0)
|
||||
@@ -1952,15 +2081,24 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
// Bei CLI-Fehler ohne Output → Fehlermeldung an Client senden
|
||||
if (!result.Success && fullText.Length == 0 && !isUserChoice)
|
||||
// Bei CLI-Fehler → Fehlermeldung an Client senden und in DB speichern
|
||||
if (!result.Success && !isUserChoice)
|
||||
{
|
||||
var errorMsg = !string.IsNullOrEmpty(result.Error) ? result.Error : "CLI hat keine Antwort geliefert";
|
||||
// Error in DB speichern (damit er beim Neuladen sichtbar ist)
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Iteration = 1,
|
||||
Role = "system",
|
||||
Type = "error",
|
||||
Content = $"CLI-Fehler: {errorMsg}"
|
||||
});
|
||||
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {errorMsg}" }, cancellationToken);
|
||||
}
|
||||
|
||||
// Antwort in DB speichern (nur wenn nicht schon im UserChoice-Handler gespeichert)
|
||||
if (!isUserChoice && result.Success && fullText.Length > 0)
|
||||
if (!isUserChoice && fullText.Length > 0)
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
@@ -2010,9 +2148,118 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SseHelper.TryWriteSse(response, "error", new { error = $"CLI-Fehler: {ex.Message}" }, cancellationToken);
|
||||
var errorMsg = $"CLI-Fehler: {ex.Message}";
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Iteration = 1,
|
||||
Role = "system",
|
||||
Type = "error",
|
||||
Content = errorMsg
|
||||
});
|
||||
await SseHelper.TryWriteSse(response, "error", new { error = errorMsg }, cancellationToken);
|
||||
await FinalizeChain(chain, usage, 1, "error", sw);
|
||||
await SseHelper.TryWriteSse(response, "done", new { chainId }, cancellationToken);
|
||||
await SseHelper.TryWriteSse(response, "done", new
|
||||
{
|
||||
model = modelId,
|
||||
chainId,
|
||||
durationMs = sw.ElapsedMilliseconds,
|
||||
stopReason = "error",
|
||||
viaCli = true
|
||||
}, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Temp-Bilddateien aufräumen
|
||||
CleanupTempImages(tempImagePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mappt CLI-Tool-Namen (Edit, Write, Bash, etc.) auf gott-Tool-Namen (edit_file, write_file, etc.).
|
||||
/// </summary>
|
||||
private static string MapCliToolName(string cliName)
|
||||
{
|
||||
return cliName switch
|
||||
{
|
||||
"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",
|
||||
_ => cliName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mappt CLI-Tool-Input-Felder auf gott-Konventionen (z.B. file_path→path, old_string→old_text).
|
||||
/// </summary>
|
||||
private static string MapCliToolInput(string cliToolName, string inputJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inputJson)) return inputJson;
|
||||
|
||||
try
|
||||
{
|
||||
var input = JsonSerializer.Deserialize<JsonElement>(inputJson);
|
||||
if (input.ValueKind != JsonValueKind.Object) return inputJson;
|
||||
|
||||
switch (cliToolName)
|
||||
{
|
||||
case "Edit":
|
||||
{
|
||||
// file_path → path, old_string → old_text, new_string → new_text
|
||||
var mapped = new Dictionary<string, object>();
|
||||
foreach (var prop in input.EnumerateObject())
|
||||
{
|
||||
var key = prop.Name switch
|
||||
{
|
||||
"file_path" => "path",
|
||||
"old_string" => "old_text",
|
||||
"new_string" => "new_text",
|
||||
_ => prop.Name
|
||||
};
|
||||
mapped[key] = prop.Value;
|
||||
}
|
||||
return JsonSerializer.Serialize(mapped);
|
||||
}
|
||||
case "Write":
|
||||
{
|
||||
// file_path → path, content bleibt
|
||||
var mapped = new Dictionary<string, object>();
|
||||
foreach (var prop in input.EnumerateObject())
|
||||
{
|
||||
var key = prop.Name == "file_path" ? "path" : prop.Name;
|
||||
mapped[key] = prop.Value;
|
||||
}
|
||||
return JsonSerializer.Serialize(mapped);
|
||||
}
|
||||
case "Read":
|
||||
{
|
||||
// file_path → path
|
||||
var mapped = new Dictionary<string, object>();
|
||||
foreach (var prop in input.EnumerateObject())
|
||||
{
|
||||
var key = prop.Name == "file_path" ? "path" : prop.Name;
|
||||
mapped[key] = prop.Value;
|
||||
}
|
||||
return JsonSerializer.Serialize(mapped);
|
||||
}
|
||||
case "Bash":
|
||||
{
|
||||
// command bleibt gleich
|
||||
return inputJson;
|
||||
}
|
||||
default:
|
||||
return inputJson;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return inputJson;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,9 +234,20 @@ public class ClaudeCliService
|
||||
}
|
||||
else if (cbType == "tool_use")
|
||||
{
|
||||
// Text-Block vorher abschließen
|
||||
// Text-Block vorher abschließen + lineBuffer flushen
|
||||
if (inTextBlock)
|
||||
{
|
||||
// Restlichen Text im lineBuffer emittieren bevor Tool startet
|
||||
if (lineBuffer.Length > 0)
|
||||
{
|
||||
var remaining = lineBuffer.ToString();
|
||||
lineBuffer.Clear();
|
||||
if (remaining.Trim().Length > 0)
|
||||
{
|
||||
fullOutput.Append(remaining);
|
||||
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = remaining });
|
||||
}
|
||||
}
|
||||
inTextBlock = false;
|
||||
await onEvent(new CliStreamEvent { Type = CliEventType.TextEnd });
|
||||
}
|
||||
@@ -361,8 +372,18 @@ public class ClaudeCliService
|
||||
}
|
||||
else if (currentBlockType == "text")
|
||||
{
|
||||
// lineBuffer flushen (Text ohne trailing \n)
|
||||
if (lineBuffer.Length > 0)
|
||||
{
|
||||
var remaining = lineBuffer.ToString();
|
||||
lineBuffer.Clear();
|
||||
if (remaining.Trim().Length > 0)
|
||||
{
|
||||
fullOutput.Append(remaining);
|
||||
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = remaining });
|
||||
}
|
||||
}
|
||||
currentBlockType = null;
|
||||
// text_end wird erst beim nächsten tool_use oder am Ende gesendet
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -439,6 +460,48 @@ public class ClaudeCliService
|
||||
if (isError)
|
||||
result.Error = json.TryGetProperty("result", out var errMsg) ? errMsg.GetString() : "CLI error";
|
||||
}
|
||||
else if (type == "user" && json.TryGetProperty("message", out var userMsg))
|
||||
{
|
||||
// CLI sendet tool_result als type:"user" message
|
||||
if (userMsg.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in content.EnumerateArray())
|
||||
{
|
||||
var itemType = item.TryGetProperty("type", out var iType) ? iType.GetString() : null;
|
||||
if (itemType == "tool_result")
|
||||
{
|
||||
var toolUseId = item.TryGetProperty("tool_use_id", out var tuId) ? tuId.GetString() : "";
|
||||
var resultIsError = item.TryGetProperty("is_error", out var isErr) && isErr.GetBoolean();
|
||||
|
||||
// Content kann string oder array sein
|
||||
var resultContent = "";
|
||||
if (item.TryGetProperty("content", out var rc))
|
||||
{
|
||||
if (rc.ValueKind == JsonValueKind.String)
|
||||
resultContent = rc.GetString();
|
||||
else if (rc.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var part in rc.EnumerateArray())
|
||||
{
|
||||
if (part.TryGetProperty("text", out var partText))
|
||||
sb.Append(partText.GetString());
|
||||
}
|
||||
resultContent = sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
await onEvent(new CliStreamEvent
|
||||
{
|
||||
Type = CliEventType.ToolResult,
|
||||
ToolId = toolUseId,
|
||||
ToolResultContent = resultContent,
|
||||
ToolResultIsError = resultIsError
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
@@ -488,6 +551,17 @@ public class ClaudeCliService
|
||||
}
|
||||
|
||||
// Sicherstellen, dass offene Blöcke geschlossen werden
|
||||
if (currentBlockType == "tool_use" && currentToolName != null)
|
||||
{
|
||||
await onEvent(new CliStreamEvent
|
||||
{
|
||||
Type = CliEventType.ToolEnd,
|
||||
ToolName = currentToolName,
|
||||
ToolId = currentToolId,
|
||||
ToolInput = currentToolInput.ToString()
|
||||
});
|
||||
currentBlockType = null;
|
||||
}
|
||||
if (inTextBlock)
|
||||
await onEvent(new CliStreamEvent { Type = CliEventType.TextEnd });
|
||||
|
||||
@@ -613,6 +687,7 @@ public enum CliEventType
|
||||
TextEnd,
|
||||
ToolStart,
|
||||
ToolEnd,
|
||||
ToolResult,
|
||||
UserChoice
|
||||
}
|
||||
|
||||
@@ -623,6 +698,8 @@ public class CliStreamEvent
|
||||
public string ToolName { get; set; }
|
||||
public string ToolId { get; set; }
|
||||
public string ToolInput { get; set; }
|
||||
public string ToolResultContent { get; set; }
|
||||
public bool ToolResultIsError { get; set; }
|
||||
public string ChoiceQuestion { get; set; }
|
||||
public List<string> ChoiceOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user