feat: MCP-Tools ask_user_choice + memory_read/write/list, Text-Marker entfernt

MCP-Server erweitert um ask_user_choice (Long-Poll), memory_read, memory_write
und memory_list Tools. MCP-Config wird jetzt immer generiert (nicht nur bei
Remote-Verbindung). InternalEndpoint um PendingChoices + choice-respond Endpoint
ergänzt. Frontend differenziert CLI-Modus (choice-respond API) vs. API-Modus.
Gesamter :::ASK_USER_CHOICE::: Text-Marker-Mechanismus aus ClaudeCliService,
AnthropicService, markdown.js und stream-ui.js entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-18 09:54:30 +01:00
1 parent 3b068a7866
commit 3e0a69e7a4
10 files changed
+493 -319

No files matched your search

+16 -120
View File
@@ -113,7 +113,14 @@ public class ClaudeCliService
}
if (webSearch) { tools.Add("WebFetch"); tools.Add("WebSearch"); }
// MCP-Tools erlauben (wenn MCP-Config vorhanden)
if (!string.IsNullOrEmpty(mcpConfigPath)) { tools.Add("mcp__gott-remote__remote_execute"); }
if (!string.IsNullOrEmpty(mcpConfigPath))
{
tools.Add("mcp__gott-remote__remote_execute");
tools.Add("mcp__gott-remote__ask_user_choice");
tools.Add("mcp__gott-remote__memory_read");
tools.Add("mcp__gott-remote__memory_write");
tools.Add("mcp__gott-remote__memory_list");
}
args.Add("--allowed-tools");
args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\"");
@@ -161,11 +168,7 @@ public class ClaudeCliService
var currentToolInput = new StringBuilder();
bool inTextBlock = false;
// Tracking für ask_user_choice Marker-Erkennung
var lineBuffer = new StringBuilder();
bool inChoiceCapture = false;
var choiceJsonBuffer = new StringBuilder();
bool userChoiceDetected = false;
try
{
@@ -286,9 +289,8 @@ public class ClaudeCliService
if (deltaType == "text_delta" && delta.TryGetProperty("text", out var txt))
{
var text = txt.GetString();
if (!string.IsNullOrEmpty(text) && !userChoiceDetected)
if (!string.IsNullOrEmpty(text))
{
// Zeilenbasierte Marker-Erkennung für :::ASK_USER_CHOICE:::
for (int ci = 0; ci < text.Length; ci++)
{
var ch = text[ci];
@@ -297,52 +299,8 @@ public class ClaudeCliService
var completedLine = lineBuffer.ToString().Trim();
lineBuffer.Clear();
if (inChoiceCapture)
if (completedLine.Length > 0 || fullOutput.Length > 0)
{
if (completedLine == ":::END_ASK_USER_CHOICE:::")
{
// JSON parsen und UserChoice-Event emittieren
try
{
var choiceJson = JsonSerializer.Deserialize<JsonElement>(choiceJsonBuffer.ToString());
var question = choiceJson.TryGetProperty("question", out var cq) ? cq.GetString() ?? "" : "";
var choices = new List<string>();
if (choiceJson.TryGetProperty("choices", out var cc) && cc.ValueKind == JsonValueKind.Array)
foreach (var cOpt in cc.EnumerateArray())
choices.Add(cOpt.GetString() ?? "");
userChoiceDetected = true;
await onEvent(new CliStreamEvent
{
Type = CliEventType.UserChoice,
ChoiceQuestion = question,
ChoiceOptions = choices
});
}
catch (JsonException)
{
// Fehlerhaftes JSON → als normalen Text weiterleiten
var fallback = $":::ASK_USER_CHOICE:::\n{choiceJsonBuffer}\n:::END_ASK_USER_CHOICE:::\n";
fullOutput.Append(fallback);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = fallback });
}
inChoiceCapture = false;
choiceJsonBuffer.Clear();
}
else
{
choiceJsonBuffer.AppendLine(completedLine);
}
}
else if (completedLine == ":::ASK_USER_CHOICE:::")
{
inChoiceCapture = true;
choiceJsonBuffer.Clear();
}
else if (completedLine.Length > 0 || fullOutput.Length > 0)
{
// Normale Zeile → Text weiterleiten (inkl. Newline)
// Leere Zeilen am Anfang überspringen (vor dem eigentlichen Text)
var lineText = completedLine + "\n";
fullOutput.Append(lineText);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = lineText });
@@ -401,41 +359,12 @@ public class ClaudeCliService
else if (type == "result")
{
// Restlichen lineBuffer flushen (nicht-abgeschlossene Zeile)
if (lineBuffer.Length > 0 && !userChoiceDetected)
if (lineBuffer.Length > 0)
{
var remaining = lineBuffer.ToString().Trim();
lineBuffer.Clear();
if (inChoiceCapture && remaining == ":::END_ASK_USER_CHOICE:::")
{
// End-Marker ohne trailing \n → Capture abschließen
try
{
var choiceJson = JsonSerializer.Deserialize<JsonElement>(choiceJsonBuffer.ToString());
var question = choiceJson.TryGetProperty("question", out var cq) ? cq.GetString() ?? "" : "";
var choices = new List<string>();
if (choiceJson.TryGetProperty("choices", out var cc) && cc.ValueKind == JsonValueKind.Array)
foreach (var cOpt in cc.EnumerateArray())
choices.Add(cOpt.GetString() ?? "");
userChoiceDetected = true;
await onEvent(new CliStreamEvent
{
Type = CliEventType.UserChoice,
ChoiceQuestion = question,
ChoiceOptions = choices
});
}
catch (JsonException)
{
var fallback = $":::ASK_USER_CHOICE:::\n{choiceJsonBuffer}\n{remaining}\n";
fullOutput.Append(fallback);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = fallback });
}
inChoiceCapture = false;
choiceJsonBuffer.Clear();
}
else if (!inChoiceCapture)
if (remaining.Length > 0)
{
fullOutput.Append(remaining);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = remaining });
@@ -519,40 +448,12 @@ public class ClaudeCliService
}
// Restlichen lineBuffer flushen
if (lineBuffer.Length > 0 && !userChoiceDetected)
if (lineBuffer.Length > 0)
{
var remaining = lineBuffer.ToString().Trim();
lineBuffer.Clear();
if (inChoiceCapture && remaining == ":::END_ASK_USER_CHOICE:::")
{
try
{
var choiceJson = JsonSerializer.Deserialize<JsonElement>(choiceJsonBuffer.ToString());
var question = choiceJson.TryGetProperty("question", out var cq) ? cq.GetString() ?? "" : "";
var choices = new List<string>();
if (choiceJson.TryGetProperty("choices", out var cc) && cc.ValueKind == JsonValueKind.Array)
foreach (var cOpt in cc.EnumerateArray())
choices.Add(cOpt.GetString() ?? "");
userChoiceDetected = true;
await onEvent(new CliStreamEvent
{
Type = CliEventType.UserChoice,
ChoiceQuestion = question,
ChoiceOptions = choices
});
}
catch (JsonException)
{
var fallback = $":::ASK_USER_CHOICE:::\n{choiceJsonBuffer}\n{remaining}\n";
fullOutput.Append(fallback);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = fallback });
}
inChoiceCapture = false;
choiceJsonBuffer.Clear();
}
else if (!inChoiceCapture)
if (remaining.Length > 0)
{
fullOutput.Append(remaining);
await onEvent(new CliStreamEvent { Type = CliEventType.TextDelta, Text = remaining });
@@ -578,8 +479,7 @@ public class ClaudeCliService
result.ExitCode = process.ExitCode;
result.Output = fullOutput.ToString().Trim();
result.Success = process.ExitCode == 0 && (fullOutput.Length > 0 || userChoiceDetected);
result.UserChoiceDetected = userChoiceDetected;
result.Success = process.ExitCode == 0 && fullOutput.Length > 0;
if (!result.Success && string.IsNullOrEmpty(result.Error))
result.Error = error.Length > 0 ? error.ToString().Trim() : $"CLI exit code: {process.ExitCode}";
@@ -696,8 +596,7 @@ public enum CliEventType
TextEnd,
ToolStart,
ToolEnd,
ToolResult,
UserChoice
ToolResult
}
public class CliStreamEvent
@@ -709,8 +608,6 @@ public class CliStreamEvent
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; }
}
public class CliChatResult
@@ -727,7 +624,6 @@ public class CliChatResult
public long DurationApiMs { get; set; }
public string SessionId { get; set; }
public int NumTurns { get; set; }
public bool UserChoiceDetected { get; set; }
}
public class CliStatusResult