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 { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
|
||||||
const { createConversation, updateConversation } = require('../storage/conversation_store');
|
const { createConversation, updateConversation } = require('../storage/conversation_store');
|
||||||
const { applyUsage, normalizeTokenUsage } = require('../utils/token_usage');
|
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 { createIndentedWriter } = require('../ui/indented_writer');
|
||||||
const { createStatusBar } = require('../ui/status_bar');
|
const { createStatusBar } = require('../ui/status_bar');
|
||||||
|
|
||||||
@ -52,6 +52,10 @@ console.log('');
|
|||||||
|
|
||||||
let rl = null;
|
let rl = null;
|
||||||
let statusBar = null;
|
let statusBar = null;
|
||||||
|
let isRunning = false;
|
||||||
|
let escPendingCancel = false;
|
||||||
|
let activeStreamController = null;
|
||||||
|
let activeToolController = null;
|
||||||
let commandMenuActive = false;
|
let commandMenuActive = false;
|
||||||
let menuSearchTerm = '';
|
let menuSearchTerm = '';
|
||||||
let menuLastSearchTerm = '';
|
let menuLastSearchTerm = '';
|
||||||
@ -79,6 +83,16 @@ process.stdin.on('keypress', (str, key) => {
|
|||||||
}
|
}
|
||||||
return;
|
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 === '/')) {
|
if (str === '/' && (rl.line === '' || rl.line === '/')) {
|
||||||
commandMenuActive = true;
|
commandMenuActive = true;
|
||||||
if (rl) {
|
if (rl) {
|
||||||
@ -275,13 +289,30 @@ function buildApiMessages() {
|
|||||||
return messages;
|
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() {
|
async function runAssistantLoop() {
|
||||||
let continueLoop = true;
|
let continueLoop = true;
|
||||||
let firstLoop = true;
|
let firstLoop = true;
|
||||||
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
const hideCursor = () => process.stdout.write('\x1b[?25l');
|
||||||
const showCursor = () => process.stdout.write('\x1b[?25h');
|
const showCursor = () => process.stdout.write('\x1b[?25h');
|
||||||
|
isRunning = true;
|
||||||
|
escPendingCancel = false;
|
||||||
if (statusBar) statusBar.setMode('running');
|
if (statusBar) statusBar.setMode('running');
|
||||||
while (continueLoop) {
|
try {
|
||||||
|
while (continueLoop) {
|
||||||
hideCursor();
|
hideCursor();
|
||||||
const spinner = new Spinner(' ', firstLoop ? 1 : 0);
|
const spinner = new Spinner(' ', firstLoop ? 1 : 0);
|
||||||
firstLoop = false;
|
firstLoop = false;
|
||||||
@ -297,6 +328,7 @@ async function runAssistantLoop() {
|
|||||||
let usageCompletion = null;
|
let usageCompletion = null;
|
||||||
let firstContent = true;
|
let firstContent = true;
|
||||||
let assistantWriter = null;
|
let assistantWriter = null;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
const thinkingDelay = setTimeout(() => {
|
const thinkingDelay = setTimeout(() => {
|
||||||
if (state.thinkingMode) showThinkingLabel = true;
|
if (state.thinkingMode) showThinkingLabel = true;
|
||||||
@ -309,8 +341,10 @@ async function runAssistantLoop() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const messages = buildApiMessages();
|
const messages = buildApiMessages();
|
||||||
|
const streamController = new AbortController();
|
||||||
|
activeStreamController = streamController;
|
||||||
try {
|
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];
|
const choice = chunk.choices && chunk.choices[0];
|
||||||
if (!choice) continue;
|
if (!choice) continue;
|
||||||
const usage = (choice && (choice.usage || choice.delta?.usage)) || chunk.usage;
|
const usage = (choice && (choice.usage || choice.delta?.usage)) || chunk.usage;
|
||||||
@ -364,6 +398,9 @@ async function runAssistantLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err && (err.code === 'aborted' || err.name === 'AbortError' || escPendingCancel)) {
|
||||||
|
cancelled = true;
|
||||||
|
} else {
|
||||||
clearTimeout(thinkingDelay);
|
clearTimeout(thinkingDelay);
|
||||||
if (state.thinkingMode) {
|
if (state.thinkingMode) {
|
||||||
spinner.stop('○');
|
spinner.stop('○');
|
||||||
@ -373,9 +410,17 @@ async function runAssistantLoop() {
|
|||||||
showCursor();
|
showCursor();
|
||||||
console.log(`错误: ${err.message || err}`);
|
console.log(`错误: ${err.message || err}`);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activeStreamController = null;
|
||||||
|
|
||||||
clearTimeout(thinkingDelay);
|
clearTimeout(thinkingDelay);
|
||||||
|
if (cancelled) {
|
||||||
|
stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, state.thinkingMode);
|
||||||
|
showCursor();
|
||||||
|
printCancelLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!gotAnswer) {
|
if (!gotAnswer) {
|
||||||
if (state.thinkingMode) {
|
if (state.thinkingMode) {
|
||||||
spinner.stop(thinkingActive ? '○ 思考完成' : '○');
|
spinner.stop(thinkingActive ? '○ 思考完成' : '○');
|
||||||
@ -415,7 +460,16 @@ async function runAssistantLoop() {
|
|||||||
const startLine = buildStartLine(call.function.name, args);
|
const startLine = buildStartLine(call.function.name, args);
|
||||||
const finalLine = buildFinalLine(call.function.name, args);
|
const finalLine = buildFinalLine(call.function.name, args);
|
||||||
const indicator = startToolDisplay(startLine);
|
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);
|
indicator.stop(finalLine);
|
||||||
const resultLines = formatResultLines(call.function.name, args, toolResult.raw || { success: toolResult.success, error: toolResult.error });
|
const resultLines = formatResultLines(call.function.name, args, toolResult.raw || { success: toolResult.success, error: toolResult.error });
|
||||||
printResultLines(resultLines);
|
printResultLines(resultLines);
|
||||||
@ -428,6 +482,9 @@ async function runAssistantLoop() {
|
|||||||
};
|
};
|
||||||
state.messages.push(toolMsg);
|
state.messages.push(toolMsg);
|
||||||
persistConversation();
|
persistConversation();
|
||||||
|
if (escPendingCancel || (toolResult.raw && toolResult.raw.cancelled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continueLoop = true;
|
continueLoop = true;
|
||||||
hideCursor();
|
hideCursor();
|
||||||
@ -441,7 +498,13 @@ async function runAssistantLoop() {
|
|||||||
persistConversation();
|
persistConversation();
|
||||||
}
|
}
|
||||||
continueLoop = false;
|
continueLoop = false;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
showCursor();
|
||||||
|
isRunning = false;
|
||||||
|
escPendingCancel = false;
|
||||||
|
activeStreamController = null;
|
||||||
|
activeToolController = null;
|
||||||
|
if (statusBar) statusBar.setMode('input');
|
||||||
}
|
}
|
||||||
showCursor();
|
|
||||||
if (statusBar) statusBar.setMode('input');
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user