Handle ESC cancellation and tool aborts
This commit is contained in:
parent
6d2d4857f3
commit
858298c070
@ -18,7 +18,7 @@ const { renderBanner } = require('../ui/banner');
|
||||
const { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
|
||||
const { createConversation, updateConversation } = require('../storage/conversation_store');
|
||||
const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
|
||||
const { gray, cyan, green } = require('../utils/colors');
|
||||
const { gray, cyan, green, red } = require('../utils/colors');
|
||||
const { createIndentedWriter } = require('../ui/indented_writer');
|
||||
const { createStatusBar } = require('../ui/status_bar');
|
||||
|
||||
@ -52,6 +52,10 @@ console.log('');
|
||||
|
||||
let rl = null;
|
||||
let statusBar = null;
|
||||
let isRunning = false;
|
||||
let escPendingCancel = false;
|
||||
let activeStreamController = null;
|
||||
let activeToolController = null;
|
||||
let commandMenuActive = false;
|
||||
let menuSearchTerm = '';
|
||||
let menuLastSearchTerm = '';
|
||||
@ -79,6 +83,16 @@ process.stdin.on('keypress', (str, key) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key && key.name === 'escape' && isRunning) {
|
||||
escPendingCancel = true;
|
||||
if (activeStreamController && typeof activeStreamController.abort === 'function') {
|
||||
activeStreamController.abort();
|
||||
}
|
||||
if (activeToolController && typeof activeToolController.abort === 'function') {
|
||||
activeToolController.abort();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (str === '/' && (rl.line === '' || rl.line === '/')) {
|
||||
commandMenuActive = true;
|
||||
if (rl) {
|
||||
@ -275,12 +289,29 @@ function buildApiMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
function printCancelLine() {
|
||||
console.log('');
|
||||
process.stdout.write(` ${red('已取消本次响应')}\n\n`);
|
||||
}
|
||||
|
||||
function stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, thinkingMode) {
|
||||
if (!spinner) return;
|
||||
if (thinkingMode && (thinkingActive || showThinkingLabel)) {
|
||||
spinner.stop('∙ 停止思考');
|
||||
} else {
|
||||
spinner.stopSilent();
|
||||
}
|
||||
}
|
||||
|
||||
async function runAssistantLoop() {
|
||||
let continueLoop = true;
|
||||
let firstLoop = true;
|
||||
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
||||
const showCursor = () => process.stdout.write('\x1b[?25h');
|
||||
isRunning = true;
|
||||
escPendingCancel = false;
|
||||
if (statusBar) statusBar.setMode('running');
|
||||
try {
|
||||
while (continueLoop) {
|
||||
hideCursor();
|
||||
const spinner = new Spinner(' ', firstLoop ? 1 : 0);
|
||||
@ -297,6 +328,7 @@ async function runAssistantLoop() {
|
||||
let usageCompletion = null;
|
||||
let firstContent = true;
|
||||
let assistantWriter = null;
|
||||
let cancelled = false;
|
||||
|
||||
const thinkingDelay = setTimeout(() => {
|
||||
if (state.thinkingMode) showThinkingLabel = true;
|
||||
@ -309,8 +341,10 @@ async function runAssistantLoop() {
|
||||
});
|
||||
|
||||
const messages = buildApiMessages();
|
||||
const streamController = new AbortController();
|
||||
activeStreamController = streamController;
|
||||
try {
|
||||
for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode })) {
|
||||
for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode, abortSignal: streamController.signal })) {
|
||||
const choice = chunk.choices && chunk.choices[0];
|
||||
if (!choice) continue;
|
||||
const usage = (choice && (choice.usage || choice.delta?.usage)) || chunk.usage;
|
||||
@ -364,6 +398,9 @@ async function runAssistantLoop() {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && (err.code === 'aborted' || err.name === 'AbortError' || escPendingCancel)) {
|
||||
cancelled = true;
|
||||
} else {
|
||||
clearTimeout(thinkingDelay);
|
||||
if (state.thinkingMode) {
|
||||
spinner.stop('○');
|
||||
@ -374,8 +411,16 @@ async function runAssistantLoop() {
|
||||
console.log(`错误: ${err.message || err}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
activeStreamController = null;
|
||||
|
||||
clearTimeout(thinkingDelay);
|
||||
if (cancelled) {
|
||||
stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, state.thinkingMode);
|
||||
showCursor();
|
||||
printCancelLine();
|
||||
return;
|
||||
}
|
||||
if (!gotAnswer) {
|
||||
if (state.thinkingMode) {
|
||||
spinner.stop(thinkingActive ? '○ 思考完成' : '○');
|
||||
@ -415,7 +460,16 @@ async function runAssistantLoop() {
|
||||
const startLine = buildStartLine(call.function.name, args);
|
||||
const finalLine = buildFinalLine(call.function.name, args);
|
||||
const indicator = startToolDisplay(startLine);
|
||||
const toolResult = await executeTool({ workspace: WORKSPACE, config, allowMode: state.allowMode, toolCall: call });
|
||||
const toolController = new AbortController();
|
||||
activeToolController = toolController;
|
||||
const toolResult = await executeTool({
|
||||
workspace: WORKSPACE,
|
||||
config,
|
||||
allowMode: state.allowMode,
|
||||
toolCall: call,
|
||||
abortSignal: toolController.signal,
|
||||
});
|
||||
activeToolController = null;
|
||||
indicator.stop(finalLine);
|
||||
const resultLines = formatResultLines(call.function.name, args, toolResult.raw || { success: toolResult.success, error: toolResult.error });
|
||||
printResultLines(resultLines);
|
||||
@ -428,6 +482,9 @@ async function runAssistantLoop() {
|
||||
};
|
||||
state.messages.push(toolMsg);
|
||||
persistConversation();
|
||||
if (escPendingCancel || (toolResult.raw && toolResult.raw.cancelled)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
continueLoop = true;
|
||||
hideCursor();
|
||||
@ -442,6 +499,12 @@ async function runAssistantLoop() {
|
||||
}
|
||||
continueLoop = false;
|
||||
}
|
||||
} finally {
|
||||
showCursor();
|
||||
isRunning = false;
|
||||
escPendingCancel = false;
|
||||
activeStreamController = null;
|
||||
activeToolController = null;
|
||||
if (statusBar) statusBar.setMode('input');
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user