feat(chat): add /stop cancellation command across gateway and telegram

This commit is contained in:
William Valentin
2026-02-19 09:52:57 -08:00
parent 027f7ad283
commit cd74b1e78a
9 changed files with 203 additions and 11 deletions
+63 -4
View File
@@ -10,6 +10,7 @@ import { renderSafeMarkdown } from '../lib/markdown.js';
let _currentSession = null;
let _sending = false;
let _cancelling = false;
let _searchMode = false;
let _slashPopupIndex = -1;
let _elements = {};
@@ -25,6 +26,8 @@ const SLASH_COMMANDS = [
{ name: '/usage', desc: 'Show token usage' },
{ name: '/status', desc: 'Show system health' },
{ name: '/model', desc: 'Show current model' },
{ name: '/stop', desc: 'Stop active response' },
{ name: '/cancel', desc: 'Alias for /stop' },
{ name: '/approvals', desc: 'List pending guarded actions' },
{ name: '/approve', desc: 'Approve latest or by id' },
{ name: '/deny', desc: 'Deny latest or by id' },
@@ -462,6 +465,8 @@ function parseSlashCommand(text) {
case '/usage': return { type: 'usage' };
case '/status': return { type: 'status' };
case '/model': return { type: 'model', args };
case '/stop': return { type: 'stop' };
case '/cancel': return { type: 'cancel' };
case '/approvals': return { type: 'approvals' };
case '/approve': return { type: 'approve', args };
case '/deny': return { type: 'deny', args };
@@ -502,6 +507,8 @@ async function handleSlashCommand(cmd, client) {
'| `/usage` | Show token usage stats |',
'| `/status` | Show system health |',
'| `/model [tier|provider]` | Show or set model tier/provider |',
'| `/stop` | Stop active response |',
'| `/cancel` | Alias for `/stop` |',
'| `/approvals` | List pending guarded actions |',
'| `/approve [id]` | Approve latest pending or specific id |',
'| `/deny [id] [reason]` | Deny latest pending or specific id |',
@@ -562,6 +569,16 @@ async function handleSlashCommand(cmd, client) {
}
return true;
}
case 'stop':
case 'cancel': {
try {
const result = await executeAgentSlashCommand(client, 'stop');
showSystemMessage(result || 'Cancellation requested.');
} catch (err) {
showSystemMessage(`Failed to stop: ${err.message}`);
}
return true;
}
case 'approvals': {
try {
@@ -680,7 +697,8 @@ async function sendMessage(client, overrideText) {
}
_sending = true;
_elements.sendBtn.disabled = true;
_cancelling = false;
updateSendButton();
if (!overrideText && input) {input.value = '';}
// Apply search mode prefix
@@ -750,12 +768,41 @@ async function sendMessage(client, overrideText) {
placeholder.replaceWith(errorMessage);
} finally {
_sending = false;
if (_elements.sendBtn) {_elements.sendBtn.disabled = false;}
_cancelling = false;
updateSendButton();
clearPendingAttachments();
scrollToBottom();
}
}
function updateSendButton() {
if (!_elements.sendBtn) {
return;
}
if (_sending) {
_elements.sendBtn.disabled = _cancelling;
_elements.sendBtn.textContent = _cancelling ? 'Stopping...' : 'Stop';
return;
}
_elements.sendBtn.disabled = false;
_elements.sendBtn.textContent = 'Send';
}
async function cancelActiveRun(client) {
if (!_sending || _cancelling) {
return;
}
_cancelling = true;
updateSendButton();
try {
await client.call('agent.cancel', {});
} catch (err) {
showSystemMessage(`Cancel failed: ${err.message}`);
_cancelling = false;
updateSendButton();
}
}
// ── Search SVG Icon ─────────────────────────────────────────
const SEARCH_ICON = '<svg class="w-3.5 h-3.5 fill-current shrink-0" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>';
@@ -884,7 +931,13 @@ export const ChatPage = {
});
// Event: send message
_elements.sendBtn.addEventListener('click', () => sendMessage(client));
_elements.sendBtn.addEventListener('click', () => {
if (_sending) {
void cancelActiveRun(client);
return;
}
void sendMessage(client);
});
// Event: keyboard in textarea
_elements.input.addEventListener('keydown', (e) => {
@@ -924,7 +977,11 @@ export const ChatPage = {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
hideSlashPopup();
sendMessage(client);
if (_sending) {
void cancelActiveRun(client);
return;
}
void sendMessage(client);
}
});
@@ -950,11 +1007,13 @@ export const ChatPage = {
if (!_currentSession) {
_elements.messages.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">Select a session or create a new one to start chatting</div>';
}
updateSendButton();
},
teardown() {
_currentSession = null;
_sending = false;
_cancelling = false;
_searchMode = false;
_slashPopupIndex = -1;
_sessionSort = 'recent';