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:
1 parent
3b068a7866
commit
3e0a69e7a4
10 files changed
+493
-319
No files matched your search
@@ -72,6 +72,10 @@ function apiChatChoice(sessionId, choiceToolCallId, choiceResult, signal, planMo
|
||||
return fetch('/api/chat', { method: 'POST', headers: _jsonHeaders, body: JSON.stringify(payload), signal: signal });
|
||||
}
|
||||
|
||||
function apiChoiceRespond(sessionId, choice) {
|
||||
return _post('/api/chat/choice-respond', { sessionId: sessionId, choice: choice });
|
||||
}
|
||||
|
||||
function apiStopChat(sessionId) { return _post('/api/chat/stop', { sessionId: sessionId }); }
|
||||
|
||||
// ── Agent API ──
|
||||
|
||||
@@ -180,7 +180,7 @@ function addWarningBubble(text, streamed = false) {
|
||||
|
||||
/**
|
||||
* Creates a choice bubble element (for both live stream and history rendering).
|
||||
* @param {object} opts - { toolCallId, question, choices, selectedChoice, timestamp, messageId, streamed }
|
||||
* @param {object} opts - { toolCallId, question, choices, selectedChoice, timestamp, messageId, streamed, viaCli }
|
||||
*/
|
||||
function _createChoiceBubble(opts) {
|
||||
if (!Array.isArray(opts.choices)) opts.choices = [String(opts.choices)];
|
||||
@@ -189,6 +189,7 @@ function _createChoiceBubble(opts) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg choice-bubble' + (answered ? ' choice-answered' : '');
|
||||
if (opts.streamed) div.classList.add('streamed');
|
||||
if (opts.viaCli) div.dataset.viaCli = 'true';
|
||||
div.dataset.toolCallId = opts.toolCallId || '';
|
||||
if (opts.messageId) div.dataset.messageId = opts.messageId;
|
||||
|
||||
@@ -219,8 +220,8 @@ function _createChoiceBubble(opts) {
|
||||
}
|
||||
|
||||
// Wird live aus dem SSE-Stream aufgerufen
|
||||
function addChoiceBubble(toolCallId, question, choices, streamed = true) {
|
||||
const div = _createChoiceBubble({ toolCallId, question, choices, streamed });
|
||||
function addChoiceBubble(toolCallId, question, choices, streamed = true, viaCli = false) {
|
||||
const div = _createChoiceBubble({ toolCallId, question, choices, streamed, viaCli });
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
|
||||
+12
-1
@@ -78,7 +78,7 @@ function handleStreamEvent(eventType, data) {
|
||||
|
||||
case 'user_choice':
|
||||
endStreamBubble();
|
||||
addChoiceBubble(data.toolCallId, data.question, data.choices);
|
||||
addChoiceBubble(data.toolCallId, data.question, data.choices, true, data.viaCli);
|
||||
break;
|
||||
|
||||
// Preparer / Review / Finalizer events (shared pattern)
|
||||
@@ -425,6 +425,17 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
||||
});
|
||||
bubbleDiv.classList.add('choice-answered');
|
||||
|
||||
// CLI-Modus: Antwort über internen Endpoint an MCP-Server weiterleiten (CLI läuft weiter)
|
||||
if (bubbleDiv.dataset.viaCli === 'true') {
|
||||
try {
|
||||
await apiChoiceRespond(getCurrentSessionId(), choice);
|
||||
} catch (e) {
|
||||
console.error('Choice respond failed:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// API-Modus: Normaler Chat-Flow (neue Chain)
|
||||
await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal, _planModeActive));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,6 @@ function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
var src = text.trim();
|
||||
|
||||
// 0) :::ASK_USER_CHOICE::: Blöcke erkennen und als Platzhalter speichern (vor Markdown-Parsing)
|
||||
var choiceBlocks = [];
|
||||
src = src.replace(/:::ASK_USER_CHOICE:::\s*\n?([\s\S]*?)\n?\s*:::END_ASK_USER_CHOICE:::/g, function(match, jsonStr) {
|
||||
try {
|
||||
var data = JSON.parse(jsonStr.trim());
|
||||
choiceBlocks.push(data);
|
||||
return '%%CHOICE' + (choiceBlocks.length - 1) + '%%';
|
||||
} catch(e) {
|
||||
return match; // JSON-Fehler → als Text belassen
|
||||
}
|
||||
});
|
||||
|
||||
// 1) LaTeX-Ausdrücke VOR dem Markdown-Parsing schützen
|
||||
// Reihenfolge: zuerst Block ($$...$$), dann Inline ($...$)
|
||||
var mathBlocks = [];
|
||||
@@ -107,22 +95,6 @@ function renderMarkdown(text) {
|
||||
}
|
||||
});
|
||||
|
||||
// 8) Choice-Platzhalter durch interaktive Buttons ersetzen
|
||||
html = html.replace(/%%CHOICE(\d+)%%/g, function(m, idx) {
|
||||
var data = choiceBlocks[parseInt(idx)];
|
||||
var q = data.question || '';
|
||||
var choices = data.choices || [];
|
||||
var jsonAttr = esc(JSON.stringify(data));
|
||||
var out = '<div class="inline-choice-block" data-choice-json="' + jsonAttr + '">';
|
||||
out += '<div class="choice-question">' + esc(q) + '</div>';
|
||||
out += '<div class="choice-buttons">';
|
||||
for (var i = 0; i < choices.length; i++) {
|
||||
out += '<button class="choice-btn inline-choice-btn" data-choice="' + esc(choices[i]) + '">' + esc(choices[i]) + '</button>';
|
||||
}
|
||||
out += '</div></div>';
|
||||
return out;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,7 @@ function appendToStream(text) {
|
||||
if (!_streamDiv) startStreamBubble();
|
||||
_streamText += text;
|
||||
|
||||
// Wenn ein :::ASK_USER_CHOICE::: Block begonnen aber noch nicht abgeschlossen wurde,
|
||||
// nur den Text VOR dem Marker rendern (Marker-Block verstecken bis er komplett ist)
|
||||
var displayText = _streamText;
|
||||
var choiceStart = displayText.indexOf(':::ASK_USER_CHOICE:::');
|
||||
if (choiceStart >= 0 && displayText.indexOf(':::END_ASK_USER_CHOICE:::') < 0) {
|
||||
displayText = displayText.substring(0, choiceStart);
|
||||
}
|
||||
|
||||
_streamDiv.innerHTML = renderMarkdown(displayText);
|
||||
_streamDiv.innerHTML = renderMarkdown(_streamText);
|
||||
// Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht
|
||||
const cursor = document.createElement('span');
|
||||
cursor.className = 'cursor';
|
||||
|
||||
Reference in new issue
Block a user