feat: MCP-Server für remote_execute + Inline-Choice-UI
- Python MCP-Server (stdio, JSON-RPC 2.0) für remote_execute via Claude CLI - Interner API-Endpoint /api/internal/remote-execute mit Token-Auth - MCP-Config wird automatisch generiert wenn Remote-Verbindung vorhanden - Auth-Middleware: /api/internal/ von Auth ausgenommen - Frontend: :::ASK_USER_CHOICE::: Blöcke als interaktive Buttons rendern - Streaming: Unvollständige Choice-Blöcke werden während des Streams versteckt - tool-ui.js: MCP-Tool-Namen generisch normalisieren (mcp__server__tool → tool) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee0b817f25
commit
3b068a7866
14 files changed
+576
-6
No files matched your search
@@ -1280,6 +1280,14 @@
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.inline-choice-block {
|
||||
margin: var(--gap) 0;
|
||||
padding: var(--gap);
|
||||
border-left: 3px solid var(--primary);
|
||||
background: var(--f1-bg);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* ── OpenClaw Import Badge ── */
|
||||
|
||||
.openclaw-badge {
|
||||
|
||||
@@ -402,6 +402,14 @@ async function sendMessage() {
|
||||
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive));
|
||||
}
|
||||
|
||||
// ── Direct Message (für Inline-Choice-Buttons) ──
|
||||
async function sendMessageDirect(text) {
|
||||
if (isStreaming || !text) return;
|
||||
addUserBubble(text, true);
|
||||
scrollDownForce();
|
||||
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
|
||||
}
|
||||
|
||||
// ── Choice Click Handler ──
|
||||
|
||||
async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
||||
@@ -419,3 +427,23 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
||||
|
||||
await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal, _planModeActive));
|
||||
}
|
||||
|
||||
// ── Inline Choice Blocks (Fallback aus Markdown-Rendering) ──
|
||||
// Event-Delegation für .inline-choice-btn Buttons die von renderMarkdown() erzeugt wurden
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.inline-choice-btn');
|
||||
if (!btn) return;
|
||||
var block = btn.closest('.inline-choice-block');
|
||||
if (!block || block.classList.contains('choice-answered')) return;
|
||||
|
||||
var choice = btn.dataset.choice;
|
||||
// Buttons deaktivieren + markieren
|
||||
block.querySelectorAll('.inline-choice-btn').forEach(function(b) {
|
||||
b.disabled = true;
|
||||
b.classList.add(b === btn ? 'choice-selected' : 'choice-unselected');
|
||||
});
|
||||
block.classList.add('choice-answered');
|
||||
|
||||
// Inline-Choice als normale User-Nachricht senden (kein tool_use in DB vorhanden)
|
||||
sendMessageDirect(choice);
|
||||
});
|
||||
@@ -6,6 +6,18 @@ 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 = [];
|
||||
@@ -95,6 +107,22 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -15,7 +15,16 @@ function startStreamBubble() {
|
||||
function appendToStream(text) {
|
||||
if (!_streamDiv) startStreamBubble();
|
||||
_streamText += text;
|
||||
_streamDiv.innerHTML = renderMarkdown(_streamText);
|
||||
|
||||
// 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);
|
||||
// Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht
|
||||
const cursor = document.createElement('span');
|
||||
cursor.className = 'cursor';
|
||||
|
||||
@@ -10,12 +10,19 @@ const CLI_TOOL_NAME_MAP = {
|
||||
'WebSearch': 'web_search',
|
||||
'WebFetch': 'web_fetch',
|
||||
'NotebookEdit': 'notebook_edit',
|
||||
'ToolSearch': 'tool_search'
|
||||
'ToolSearch': 'tool_search',
|
||||
'mcp__gott-remote__remote_execute': 'remote_execute'
|
||||
};
|
||||
|
||||
/** Normalize CLI tool name to gott convention (Edit → edit_file, etc.) */
|
||||
function normalizeToolName(name) {
|
||||
return CLI_TOOL_NAME_MAP[name] || 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.) */
|
||||
|
||||
Reference in new issue
Block a user