Fix command menu prompt residue

This commit is contained in:
JOJO 2026-02-28 12:36:50 +08:00
parent c903041474
commit 136a503846

View File

@ -17,6 +17,7 @@ const { Spinner, truncateThinking } = require('../ui/spinner');
const { renderBanner } = require('../ui/banner'); 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 { gray, cyan, green } = require('../utils/colors'); const { gray, cyan, green } = require('../utils/colors');
const { createIndentedWriter } = require('../ui/indented_writer'); const { createIndentedWriter } = require('../ui/indented_writer');
@ -24,6 +25,7 @@ const WORKSPACE = process.cwd();
const WORKSPACE_NAME = path.basename(WORKSPACE); const WORKSPACE_NAME = path.basename(WORKSPACE);
const USERNAME = os.userInfo().username || 'user'; const USERNAME = os.userInfo().username || 'user';
const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `; const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `;
const MENU_PAGE_SIZE = 6;
const config = ensureConfig(); const config = ensureConfig();
const state = createState(config, WORKSPACE); const state = createState(config, WORKSPACE);
@ -48,6 +50,7 @@ renderBanner({
let rl = null; let rl = null;
let commandMenuActive = false; let commandMenuActive = false;
let menuSearchTerm = ''; let menuSearchTerm = '';
let menuLastSearchTerm = '';
let menuJustClosedAt = 0; let menuJustClosedAt = 0;
let menuInjectedCommand = null; let menuInjectedCommand = null;
let menuAbortController = null; let menuAbortController = null;
@ -68,12 +71,19 @@ process.stdin.on('keypress', (str, key) => {
} }
if (str === '/' && (rl.line === '' || rl.line === '/')) { if (str === '/' && (rl.line === '' || rl.line === '/')) {
commandMenuActive = true; commandMenuActive = true;
if (rl) {
rl.pause();
rl.line = '';
rl.cursor = 0;
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
}
menuSearchTerm = ''; menuSearchTerm = '';
menuAbortController = new AbortController(); menuAbortController = new AbortController();
void openCommandMenu({ void openCommandMenu({
rl, rl,
prompt: PROMPT, prompt: PROMPT,
pageSize: 6, pageSize: MENU_PAGE_SIZE,
colorEnabled: process.stdout.isTTY, colorEnabled: process.stdout.isTTY,
resetAnsi: '\x1b[0m', resetAnsi: '\x1b[0m',
onInput: (input) => { onInput: (input) => {
@ -90,12 +100,25 @@ process.stdin.on('keypress', (str, key) => {
commandMenuActive = false; commandMenuActive = false;
menuAbortController = null; menuAbortController = null;
menuJustClosedAt = menuInjectedCommand ? Date.now() : 0; menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
menuLastSearchTerm = menuSearchTerm;
drainStdin(); drainStdin();
rl.line = ''; rl.line = '';
rl.cursor = 0; rl.cursor = 0;
menuSearchTerm = ''; menuSearchTerm = '';
readline.clearLine(process.stdout, 0); if (process.stdout.isTTY) {
readline.cursorTo(process.stdout, 0); readline.clearScreenDown(process.stdout);
}
// Clear possible echoes from the base readline line (current + previous line).
if (process.stdout.isTTY) {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
} else {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
}
rl.prompt(true); rl.prompt(true);
if (menuInjectedCommand) { if (menuInjectedCommand) {
const injected = menuInjectedCommand; const injected = menuInjectedCommand;
@ -123,21 +146,31 @@ function initReadline() {
rl.prompt(); rl.prompt();
rl.on('line', async (line) => { rl.on('line', async (line) => {
if (rl) {
rl.line = '';
rl.cursor = 0;
}
if (commandMenuActive) { if (commandMenuActive) {
rl.prompt();
return; return;
} }
const input = line.trim(); const input = line.trim();
if (menuJustClosedAt) { if (menuJustClosedAt) {
const tooOld = Date.now() - menuJustClosedAt > 800; const tooOld = Date.now() - menuJustClosedAt > 800;
const normalizedMenu = String(menuSearchTerm).trim().replace(/^\/+/, ''); const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, '');
const normalizedInput = input.replace(/^\/+/, ''); const normalizedInput = input.replace(/^\/+/, '');
if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) { if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
menuJustClosedAt = 0; menuJustClosedAt = 0;
menuLastSearchTerm = '';
if (process.stdout.isTTY) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
}
rl.prompt(); rl.prompt();
return; return;
} }
menuJustClosedAt = 0; menuJustClosedAt = 0;
menuLastSearchTerm = '';
} }
if (!input) { if (!input) {
rl.prompt(); rl.prompt();
@ -235,6 +268,8 @@ async function runAssistantLoop() {
let assistantContent = ''; let assistantContent = '';
let toolCalls = {}; let toolCalls = {};
let usageTotal = null; let usageTotal = null;
let usagePrompt = null;
let usageCompletion = null;
let firstContent = true; let firstContent = true;
let assistantWriter = null; let assistantWriter = null;
@ -253,7 +288,12 @@ async function runAssistantLoop() {
for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode })) { for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode })) {
const choice = chunk.choices && chunk.choices[0]; const choice = chunk.choices && chunk.choices[0];
if (!choice) continue; if (!choice) continue;
if (chunk.usage && chunk.usage.total_tokens) usageTotal = chunk.usage.total_tokens; const usage = (choice && (choice.usage || choice.delta?.usage)) || chunk.usage;
if (usage) {
if (Number.isFinite(usage.prompt_tokens)) usagePrompt = usage.prompt_tokens;
if (Number.isFinite(usage.completion_tokens)) usageCompletion = usage.completion_tokens;
if (Number.isFinite(usage.total_tokens)) usageTotal = usage.total_tokens;
}
const delta = choice.delta || {}; const delta = choice.delta || {};
if (delta.reasoning_content || delta.reasoning_details) { if (delta.reasoning_content || delta.reasoning_details) {
@ -322,7 +362,13 @@ async function runAssistantLoop() {
} }
showCursor(); showCursor();
if (usageTotal) state.tokenUsage = usageTotal; if (usageTotal !== null || usagePrompt !== null || usageCompletion !== null) {
state.tokenUsage = applyUsage(normalizeTokenUsage(state.tokenUsage), {
prompt_tokens: usagePrompt,
completion_tokens: usageCompletion,
total_tokens: usageTotal,
});
}
const toolCallList = Object.keys(toolCalls) const toolCallList = Object.keys(toolCalls)
.map((k) => Number(k)) .map((k) => Number(k))