From 136a503846641faa956ae3c9b2c999de3281acc1 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sat, 28 Feb 2026 12:36:50 +0800 Subject: [PATCH] Fix command menu prompt residue --- src/cli/index.js | 60 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/src/cli/index.js b/src/cli/index.js index 2d0590d..443e54c 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -17,6 +17,7 @@ const { Spinner, truncateThinking } = require('../ui/spinner'); 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 { createIndentedWriter } = require('../ui/indented_writer'); @@ -24,6 +25,7 @@ const WORKSPACE = process.cwd(); const WORKSPACE_NAME = path.basename(WORKSPACE); const USERNAME = os.userInfo().username || 'user'; const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `; +const MENU_PAGE_SIZE = 6; const config = ensureConfig(); const state = createState(config, WORKSPACE); @@ -48,6 +50,7 @@ renderBanner({ let rl = null; let commandMenuActive = false; let menuSearchTerm = ''; +let menuLastSearchTerm = ''; let menuJustClosedAt = 0; let menuInjectedCommand = null; let menuAbortController = null; @@ -68,12 +71,19 @@ process.stdin.on('keypress', (str, key) => { } if (str === '/' && (rl.line === '' || rl.line === '/')) { commandMenuActive = true; + if (rl) { + rl.pause(); + rl.line = ''; + rl.cursor = 0; + readline.clearLine(process.stdout, 0); + readline.cursorTo(process.stdout, 0); + } menuSearchTerm = ''; menuAbortController = new AbortController(); void openCommandMenu({ rl, prompt: PROMPT, - pageSize: 6, + pageSize: MENU_PAGE_SIZE, colorEnabled: process.stdout.isTTY, resetAnsi: '\x1b[0m', onInput: (input) => { @@ -90,12 +100,25 @@ process.stdin.on('keypress', (str, key) => { commandMenuActive = false; menuAbortController = null; menuJustClosedAt = menuInjectedCommand ? Date.now() : 0; + menuLastSearchTerm = menuSearchTerm; drainStdin(); rl.line = ''; rl.cursor = 0; menuSearchTerm = ''; - readline.clearLine(process.stdout, 0); - readline.cursorTo(process.stdout, 0); + if (process.stdout.isTTY) { + 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); if (menuInjectedCommand) { const injected = menuInjectedCommand; @@ -123,21 +146,31 @@ function initReadline() { rl.prompt(); rl.on('line', async (line) => { + if (rl) { + rl.line = ''; + rl.cursor = 0; + } if (commandMenuActive) { - rl.prompt(); return; } const input = line.trim(); if (menuJustClosedAt) { const tooOld = Date.now() - menuJustClosedAt > 800; - const normalizedMenu = String(menuSearchTerm).trim().replace(/^\/+/, ''); + const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, ''); const normalizedInput = input.replace(/^\/+/, ''); if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) { 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(); return; } menuJustClosedAt = 0; + menuLastSearchTerm = ''; } if (!input) { rl.prompt(); @@ -235,6 +268,8 @@ async function runAssistantLoop() { let assistantContent = ''; let toolCalls = {}; let usageTotal = null; + let usagePrompt = null; + let usageCompletion = null; let firstContent = true; let assistantWriter = null; @@ -253,7 +288,12 @@ async function runAssistantLoop() { for await (const chunk of streamChat({ config, messages, tools, thinkingMode: state.thinkingMode })) { const choice = chunk.choices && chunk.choices[0]; 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 || {}; if (delta.reasoning_content || delta.reasoning_details) { @@ -322,7 +362,13 @@ async function runAssistantLoop() { } 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) .map((k) => Number(k))