fix: vision(#623): disinto-chat escalation tools (CI run, issue create, PR create) (#712)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/smoke-init Pipeline was successful

This commit is contained in:
Claude 2026-04-12 03:56:07 +00:00
parent 9d778f6fd6
commit 8649b278a0
3 changed files with 453 additions and 1 deletions

View file

@ -161,6 +161,56 @@
white-space: pre-wrap;
word-wrap: break-word;
}
/* Action button container */
.action-buttons {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.action-btn {
background: #0f3460;
border: 1px solid #e94560;
color: #e94560;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.action-btn:hover {
background: #e94560;
color: white;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn .spinner {
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.action-btn.success {
background: #1a1a2e;
border-color: #4ade80;
color: #4ade80;
}
.action-btn.error {
background: #1a1a2e;
border-color: #f87171;
color: #f87171;
}
.input-area {
display: flex;
gap: 0.5rem;
@ -404,11 +454,28 @@
function addMessage(role, content, streaming = false) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
// Parse action markers if this is an assistant message
let contentHtml = escapeHtml(content);
let actions = [];
if (role === 'assistant' && !streaming) {
const parsed = parseActionMarkers(content, messagesDiv.children.length);
contentHtml = parsed.html;
actions = parsed.actions;
}
msgDiv.innerHTML = `
<div class="role">${role}</div>
<div class="content${streaming ? ' streaming' : ''}">${escapeHtml(content)}</div>
<div class="content${streaming ? ' streaming' : ''}">${contentHtml}</div>
`;
messagesDiv.appendChild(msgDiv);
// Render action buttons for assistant messages
if (actions.length > 0) {
renderActionButtons(msgDiv, actions, messagesDiv.children.length - 1);
}
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return msgDiv.querySelector('.content');
}
@ -430,6 +497,121 @@
return div.innerHTML.replace(/\n/g, '<br>');
}
// Action buttons state - track pending actions by message index
const pendingActions = new Map();
// Parse action markers from content and return HTML with action buttons
function parseActionMarkers(content, messageIndex) {
const actionPattern = /<action type="([^"]+)">(.*?)<\/action>/gs;
const hasActions = actionPattern.test(content);
if (!hasActions) {
return { html: escapeHtml(content), actions: [] };
}
// Reset pending actions for this message
pendingActions.set(messageIndex, []);
let html = content;
const actions = [];
// Replace action markers with placeholders and collect actions
html = html.replace(actionPattern, (match, type, jsonStr) => {
try {
const action = JSON.parse(jsonStr);
actions.push({ type, payload: action, id: `${messageIndex}-${actions.length}` });
// Replace with placeholder that will be rendered as button
return `<div class="action-placeholder" data-action-id="${actions[actions.length - 1].id}"></div>`;
} catch (e) {
// If JSON parsing fails, keep the original marker
return match;
}
});
// Convert newlines to <br> for HTML output
html = html.replace(/\n/g, '<br>');
return { html, actions };
}
// Render action buttons for a message
function renderActionButtons(msgDiv, actions, messageIndex) {
if (actions.length === 0) return;
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'action-buttons';
actions.forEach(action => {
const btn = document.createElement('button');
btn.className = 'action-btn';
btn.dataset.actionId = action.id;
btn.dataset.messageIndex = messageIndex;
let btnText = 'Execute';
let icon = '';
switch (action.type) {
case 'ci-run':
icon = '🚀';
btnText = `Run CI for ${action.payload.branch || 'default'}`;
break;
case 'issue-create':
icon = '📝';
btnText = `Create Issue: ${action.payload.title ? action.payload.title.substring(0, 30) + (action.payload.title.length > 30 ? '...' : '') : 'New Issue'}`;
break;
case 'pr-create':
icon = '🔀';
btnText = `Create PR: ${action.payload.title ? action.payload.title.substring(0, 30) + (action.payload.title.length > 30 ? '...' : '') : 'New PR'}`;
break;
default:
btnText = `Execute ${action.type}`;
}
btn.innerHTML = `<span>${icon}</span><span>${btnText}</span>`;
btn.addEventListener('click', () => executeAction(btn, action));
buttonsDiv.appendChild(btn);
});
msgDiv.appendChild(buttonsDiv);
}
// Execute an action
async function executeAction(btn, action) {
const messageIndex = btn.dataset.messageIndex;
const actionId = btn.dataset.actionId;
// Disable button and show loading state
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Executing...';
try {
const response = await fetch(`/chat/action/${action.type}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...action.payload,
conversation_id: currentConversationId,
}),
});
const result = await response.json();
if (result.success) {
btn.className = 'action-btn success';
btn.innerHTML = '<span></span> Executed successfully';
} else {
btn.className = 'action-btn error';
btn.innerHTML = `<span></span> Error: ${result.error || 'Unknown error'}`;
}
} catch (error) {
btn.className = 'action-btn error';
btn.innerHTML = `<span></span> Error: ${error.message}`;
}
}
// Send message handler
async function sendMessage() {
const message = textarea.value.trim();