feat(chat): add /stop cancellation command across gateway and telegram
This commit is contained in:
@@ -81,14 +81,16 @@ describe('TelegramAdapter', () => {
|
|||||||
// .use() for auth middleware
|
// .use() for auth middleware
|
||||||
expect(mockUse).toHaveBeenCalledTimes(1);
|
expect(mockUse).toHaveBeenCalledTimes(1);
|
||||||
expect(mockCatch).toHaveBeenCalledTimes(1);
|
expect(mockCatch).toHaveBeenCalledTimes(1);
|
||||||
// .command() for /start, /reset, /model, /local, /cloud, /transfer
|
// .command() for /start, /reset, /model, /local, /cloud, /transfer, /stop, /cancel
|
||||||
expect(mockCommand).toHaveBeenCalledTimes(6);
|
expect(mockCommand).toHaveBeenCalledTimes(8);
|
||||||
expect(mockCommand.mock.calls[0][0]).toBe('start');
|
expect(mockCommand.mock.calls[0][0]).toBe('start');
|
||||||
expect(mockCommand.mock.calls[1][0]).toBe('reset');
|
expect(mockCommand.mock.calls[1][0]).toBe('reset');
|
||||||
expect(mockCommand.mock.calls[2][0]).toBe('model');
|
expect(mockCommand.mock.calls[2][0]).toBe('model');
|
||||||
expect(mockCommand.mock.calls[3][0]).toBe('local');
|
expect(mockCommand.mock.calls[3][0]).toBe('local');
|
||||||
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
|
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
|
||||||
expect(mockCommand.mock.calls[5][0]).toBe('transfer');
|
expect(mockCommand.mock.calls[5][0]).toBe('transfer');
|
||||||
|
expect(mockCommand.mock.calls[6][0]).toBe('stop');
|
||||||
|
expect(mockCommand.mock.calls[7][0]).toBe('cancel');
|
||||||
// .on('message:text', ...) for text handler
|
// .on('message:text', ...) for text handler
|
||||||
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
|
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
|
||||||
// .start() to begin long polling
|
// .start() to begin long polling
|
||||||
@@ -301,6 +303,44 @@ describe('TelegramAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('/stop command forwards a stop command message', async () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
adapter.onMessage(handler);
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const stopHandler = getCommandHandler('stop');
|
||||||
|
const ctx = {
|
||||||
|
message: { message_id: 200 },
|
||||||
|
chat: { id: 100 },
|
||||||
|
from: { first_name: 'Will' },
|
||||||
|
};
|
||||||
|
await stopHandler(ctx);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||||
|
expect(msg.text).toBe('/stop');
|
||||||
|
expect(msg.metadata).toEqual({ isCommand: true, command: 'stop' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/cancel command forwards a cancel command message', async () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
adapter.onMessage(handler);
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const cancelHandler = getCommandHandler('cancel');
|
||||||
|
const ctx = {
|
||||||
|
message: { message_id: 201 },
|
||||||
|
chat: { id: 100 },
|
||||||
|
from: { first_name: 'Will' },
|
||||||
|
};
|
||||||
|
await cancelHandler(ctx);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||||
|
expect(msg.text).toBe('/stop');
|
||||||
|
expect(msg.metadata).toEqual({ isCommand: true, command: 'cancel' });
|
||||||
|
});
|
||||||
|
|
||||||
// ── Auth middleware ───────────────────────────────────────────
|
// ── Auth middleware ───────────────────────────────────────────
|
||||||
|
|
||||||
it('auth middleware blocks unauthorized chat IDs', async () => {
|
it('auth middleware blocks unauthorized chat IDs', async () => {
|
||||||
|
|||||||
@@ -261,6 +261,22 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleStopCommand = (command: 'stop' | 'cancel') => async (ctx: { message?: { message_id?: number }; chat: { id: number }; from?: { first_name?: string } }) => {
|
||||||
|
if (!this.messageHandler) {return;}
|
||||||
|
this.messageHandler({
|
||||||
|
id: String(ctx.message?.message_id ?? Date.now()),
|
||||||
|
channel: 'telegram',
|
||||||
|
senderId: String(ctx.chat.id),
|
||||||
|
senderName: ctx.from?.first_name,
|
||||||
|
text: '/stop',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: { isCommand: true, command },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
bot.command('stop', handleStopCommand('stop'));
|
||||||
|
bot.command('cancel', handleStopCommand('cancel'));
|
||||||
|
|
||||||
// ── Text message handler ──
|
// ── Text message handler ──
|
||||||
|
|
||||||
bot.on('message:text', async (ctx) => {
|
bot.on('message:text', async (ctx) => {
|
||||||
@@ -465,7 +481,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
|
|
||||||
// bot.start() is a long-running method that resolves only when the bot stops.
|
// bot.start() is a long-running method that resolves only when the bot stops.
|
||||||
// Do NOT await it — fire-and-forget so connect() resolves immediately.
|
// Do NOT await it — fire-and-forget so connect() resolves immediately.
|
||||||
bot.start({
|
const startResult = bot.start({
|
||||||
onStart: (botInfo) => {
|
onStart: (botInfo) => {
|
||||||
console.log(`Telegram bot started: @${botInfo.username}`);
|
console.log(`Telegram bot started: @${botInfo.username}`);
|
||||||
this.botInfo = { id: botInfo.id, username: botInfo.username };
|
this.botInfo = { id: botInfo.id, username: botInfo.username };
|
||||||
@@ -474,7 +490,8 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
this._lastError = undefined;
|
this._lastError = undefined;
|
||||||
this._lastErrorAt = undefined;
|
this._lastErrorAt = undefined;
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
});
|
||||||
|
Promise.resolve(startResult).catch((error) => {
|
||||||
const description = error instanceof Error ? error.message : String(error);
|
const description = error instanceof Error ? error.message : String(error);
|
||||||
this.recordAdapterError(`Telegram connect failed: ${description}`);
|
this.recordAdapterError(`Telegram connect failed: ${description}`);
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
@@ -633,7 +650,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
|
|
||||||
this._status = 'connecting';
|
this._status = 'connecting';
|
||||||
await bot.stop();
|
await bot.stop();
|
||||||
bot.start({
|
const startResult = bot.start({
|
||||||
onStart: (botInfo) => {
|
onStart: (botInfo) => {
|
||||||
console.log(`Telegram bot reconnected: @${botInfo.username}`);
|
console.log(`Telegram bot reconnected: @${botInfo.username}`);
|
||||||
this.botInfo = { id: botInfo.id, username: botInfo.username };
|
this.botInfo = { id: botInfo.id, username: botInfo.username };
|
||||||
@@ -642,7 +659,8 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
this._lastError = undefined;
|
this._lastError = undefined;
|
||||||
this._lastErrorAt = undefined;
|
this._lastErrorAt = undefined;
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
});
|
||||||
|
Promise.resolve(startResult).catch((error) => {
|
||||||
const description = error instanceof Error ? error.message : String(error);
|
const description = error instanceof Error ? error.message : String(error);
|
||||||
this.recordAdapterError(`Telegram reconnect polling error: ${description}`);
|
this.recordAdapterError(`Telegram reconnect polling error: ${description}`);
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createTransferCommand } from './index.js';
|
import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createStopCommand, createTransferCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -198,6 +198,34 @@ describe('builtin /transfer command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /stop command', () => {
|
||||||
|
it('calls cancelRun service', async () => {
|
||||||
|
const cmd = createStopCommand();
|
||||||
|
const cancelRun = vi.fn(() => 'Cancellation requested.');
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/stop',
|
||||||
|
services: { cancelRun },
|
||||||
|
});
|
||||||
|
expect(cancelRun).toHaveBeenCalledOnce();
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Cancellation requested.' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not-available when service is missing', async () => {
|
||||||
|
const cmd = createStopCommand();
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/stop',
|
||||||
|
services: {},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Stop command is not available in this session.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('builtin approval commands', () => {
|
describe('builtin approval commands', () => {
|
||||||
it('calls getApprovals for /approvals', async () => {
|
it('calls getApprovals for /approvals', async () => {
|
||||||
const cmd = createApprovalsCommand();
|
const cmd = createApprovalsCommand();
|
||||||
|
|||||||
@@ -144,6 +144,23 @@ export function createResetCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createStopCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'stop',
|
||||||
|
aliases: ['cancel'],
|
||||||
|
description: 'Stop the current in-flight run',
|
||||||
|
execute: async (_args, ctx) => {
|
||||||
|
if (!ctx.services?.cancelRun) {
|
||||||
|
return notAvailable('Stop command');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: await ctx.services.cancelRun(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createElevateCommand(): CommandDefinition {
|
export function createElevateCommand(): CommandDefinition {
|
||||||
return {
|
return {
|
||||||
name: 'elevate',
|
name: 'elevate',
|
||||||
@@ -309,6 +326,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|||||||
registry.register(createModelCommand());
|
registry.register(createModelCommand());
|
||||||
registry.register(createCompactCommand());
|
registry.register(createCompactCommand());
|
||||||
registry.register(createResetCommand());
|
registry.register(createResetCommand());
|
||||||
|
registry.register(createStopCommand());
|
||||||
registry.register(createElevateCommand());
|
registry.register(createElevateCommand());
|
||||||
registry.register(createQueueCommand());
|
registry.register(createQueueCommand());
|
||||||
registry.register(createTransferCommand());
|
registry.register(createTransferCommand());
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {
|
|||||||
createModelCommand,
|
createModelCommand,
|
||||||
createCompactCommand,
|
createCompactCommand,
|
||||||
createResetCommand,
|
createResetCommand,
|
||||||
|
createStopCommand,
|
||||||
createQueueCommand,
|
createQueueCommand,
|
||||||
createTransferCommand,
|
createTransferCommand,
|
||||||
createApprovalsCommand,
|
createApprovalsCommand,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface CommandServices {
|
|||||||
getQueue?: () => Promise<string> | string;
|
getQueue?: () => Promise<string> | string;
|
||||||
setQueue?: (input: string) => Promise<string> | string;
|
setQueue?: (input: string) => Promise<string> | string;
|
||||||
resetQueue?: () => Promise<string> | string;
|
resetQueue?: () => Promise<string> | string;
|
||||||
|
cancelRun?: () => Promise<string> | string;
|
||||||
transferSession?: (target: string) => Promise<string> | string;
|
transferSession?: (target: string) => Promise<string> | string;
|
||||||
getApprovals?: () => Promise<string> | string;
|
getApprovals?: () => Promise<string> | string;
|
||||||
approvePending?: (input: string) => Promise<string> | string;
|
approvePending?: (input: string) => Promise<string> | string;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ describe('createAgentHandlers command fast-path', () => {
|
|||||||
setBusy: vi.fn(),
|
setBusy: vi.fn(),
|
||||||
setOnToolUse: vi.fn(),
|
setOnToolUse: vi.fn(),
|
||||||
isBusy: vi.fn(() => false),
|
isBusy: vi.fn(() => false),
|
||||||
|
cancel: vi.fn(() => true),
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionManager = {
|
const sessionManager = {
|
||||||
@@ -219,6 +220,26 @@ describe('createAgentHandlers command fast-path', () => {
|
|||||||
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toContain('Reset default to: anthropic/claude-sonnet-4');
|
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toContain('Reset default to: anthropic/claude-sonnet-4');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles /stop command via fast-path and requests cancellation', async () => {
|
||||||
|
const sent: OutboundMessage[] = [];
|
||||||
|
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
||||||
|
const req: GatewayRequest = {
|
||||||
|
id: 12,
|
||||||
|
method: 'agent.send',
|
||||||
|
params: {
|
||||||
|
message: '/stop',
|
||||||
|
connectionId: 'conn-1',
|
||||||
|
metadata: { isCommand: true, command: 'stop' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlers['agent.send'](req, send);
|
||||||
|
|
||||||
|
expect(sessionBridge.cancel).toHaveBeenCalledWith('conn-1');
|
||||||
|
expect(mockAgent.process).not.toHaveBeenCalled();
|
||||||
|
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toContain('Cancellation requested');
|
||||||
|
});
|
||||||
|
|
||||||
it('falls through to agent.process for unknown commands', async () => {
|
it('falls through to agent.process for unknown commands', async () => {
|
||||||
const sent: OutboundMessage[] = [];
|
const sent: OutboundMessage[] = [];
|
||||||
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
|
||||||
|
|||||||
@@ -294,6 +294,12 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
|
|||||||
}
|
}
|
||||||
return 'Session reset.';
|
return 'Session reset.';
|
||||||
},
|
},
|
||||||
|
cancelRun: () => {
|
||||||
|
const cancelled = deps.sessionBridge.cancel(connectionId);
|
||||||
|
return cancelled
|
||||||
|
? 'Cancellation requested. The active operation will stop at the next safe point.'
|
||||||
|
: 'No active operation to cancel.';
|
||||||
|
},
|
||||||
getApprovals: () => {
|
getApprovals: () => {
|
||||||
if (!deps.hookEngine) {
|
if (!deps.hookEngine) {
|
||||||
return 'Approval gates are not enabled in this runtime.';
|
return 'Approval gates are not enabled in this runtime.';
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { renderSafeMarkdown } from '../lib/markdown.js';
|
|||||||
|
|
||||||
let _currentSession = null;
|
let _currentSession = null;
|
||||||
let _sending = false;
|
let _sending = false;
|
||||||
|
let _cancelling = false;
|
||||||
let _searchMode = false;
|
let _searchMode = false;
|
||||||
let _slashPopupIndex = -1;
|
let _slashPopupIndex = -1;
|
||||||
let _elements = {};
|
let _elements = {};
|
||||||
@@ -25,6 +26,8 @@ const SLASH_COMMANDS = [
|
|||||||
{ name: '/usage', desc: 'Show token usage' },
|
{ name: '/usage', desc: 'Show token usage' },
|
||||||
{ name: '/status', desc: 'Show system health' },
|
{ name: '/status', desc: 'Show system health' },
|
||||||
{ name: '/model', desc: 'Show current model' },
|
{ 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: '/approvals', desc: 'List pending guarded actions' },
|
||||||
{ name: '/approve', desc: 'Approve latest or by id' },
|
{ name: '/approve', desc: 'Approve latest or by id' },
|
||||||
{ name: '/deny', desc: 'Deny 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 '/usage': return { type: 'usage' };
|
||||||
case '/status': return { type: 'status' };
|
case '/status': return { type: 'status' };
|
||||||
case '/model': return { type: 'model', args };
|
case '/model': return { type: 'model', args };
|
||||||
|
case '/stop': return { type: 'stop' };
|
||||||
|
case '/cancel': return { type: 'cancel' };
|
||||||
case '/approvals': return { type: 'approvals' };
|
case '/approvals': return { type: 'approvals' };
|
||||||
case '/approve': return { type: 'approve', args };
|
case '/approve': return { type: 'approve', args };
|
||||||
case '/deny': return { type: 'deny', args };
|
case '/deny': return { type: 'deny', args };
|
||||||
@@ -502,6 +507,8 @@ async function handleSlashCommand(cmd, client) {
|
|||||||
'| `/usage` | Show token usage stats |',
|
'| `/usage` | Show token usage stats |',
|
||||||
'| `/status` | Show system health |',
|
'| `/status` | Show system health |',
|
||||||
'| `/model [tier|provider]` | Show or set model tier/provider |',
|
'| `/model [tier|provider]` | Show or set model tier/provider |',
|
||||||
|
'| `/stop` | Stop active response |',
|
||||||
|
'| `/cancel` | Alias for `/stop` |',
|
||||||
'| `/approvals` | List pending guarded actions |',
|
'| `/approvals` | List pending guarded actions |',
|
||||||
'| `/approve [id]` | Approve latest pending or specific id |',
|
'| `/approve [id]` | Approve latest pending or specific id |',
|
||||||
'| `/deny [id] [reason]` | Deny 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;
|
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': {
|
case 'approvals': {
|
||||||
try {
|
try {
|
||||||
@@ -680,7 +697,8 @@ async function sendMessage(client, overrideText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sending = true;
|
_sending = true;
|
||||||
_elements.sendBtn.disabled = true;
|
_cancelling = false;
|
||||||
|
updateSendButton();
|
||||||
if (!overrideText && input) {input.value = '';}
|
if (!overrideText && input) {input.value = '';}
|
||||||
|
|
||||||
// Apply search mode prefix
|
// Apply search mode prefix
|
||||||
@@ -750,12 +768,41 @@ async function sendMessage(client, overrideText) {
|
|||||||
placeholder.replaceWith(errorMessage);
|
placeholder.replaceWith(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
_sending = false;
|
_sending = false;
|
||||||
if (_elements.sendBtn) {_elements.sendBtn.disabled = false;}
|
_cancelling = false;
|
||||||
|
updateSendButton();
|
||||||
clearPendingAttachments();
|
clearPendingAttachments();
|
||||||
scrollToBottom();
|
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 ─────────────────────────────────────────
|
// ── 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>';
|
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
|
// 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
|
// Event: keyboard in textarea
|
||||||
_elements.input.addEventListener('keydown', (e) => {
|
_elements.input.addEventListener('keydown', (e) => {
|
||||||
@@ -924,7 +977,11 @@ export const ChatPage = {
|
|||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hideSlashPopup();
|
hideSlashPopup();
|
||||||
sendMessage(client);
|
if (_sending) {
|
||||||
|
void cancelActiveRun(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void sendMessage(client);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -950,11 +1007,13 @@ export const ChatPage = {
|
|||||||
if (!_currentSession) {
|
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>';
|
_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() {
|
teardown() {
|
||||||
_currentSession = null;
|
_currentSession = null;
|
||||||
_sending = false;
|
_sending = false;
|
||||||
|
_cancelling = false;
|
||||||
_searchMode = false;
|
_searchMode = false;
|
||||||
_slashPopupIndex = -1;
|
_slashPopupIndex = -1;
|
||||||
_sessionSort = 'recent';
|
_sessionSort = 'recent';
|
||||||
|
|||||||
Reference in New Issue
Block a user