From 41608279b1d2fbe5b04033d946fff88d8d6f707e Mon Sep 17 00:00:00 2001 From: ouyu <1986834078@qq.com> Date: Thu, 16 Apr 2026 19:52:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(=E8=BE=93=E5=85=A5=E6=A1=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BB=A4=E7=89=8C=E8=AE=A1=E6=95=B0=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在输入框底部添加令牌计数显示,使用防抖机制优化性能 --- src/components/PromptInput/PromptInput.tsx | 42 ++++++++++++++++++- .../PromptInput/PromptInputFooter.tsx | 9 +++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index cf390f11..d916a5dc 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -120,6 +120,7 @@ import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'; import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'; import { useShowFastIconHint } from './useShowFastIconHint.js'; import { useSwarmBanner } from './useSwarmBanner.js'; +import { roughTokenCountEstimation } from '../../services/tokenEstimation.js'; import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'; type Props = { debug: boolean; @@ -250,6 +251,45 @@ function PromptInput({ show: false }); const [cursorOffset, setCursorOffset] = useState(input.length); + const [tokenCount, setTokenCount] = useState(input.length === 0 ? 0 : roughTokenCountEstimation(input)); + const [isCalculatingTokens, setIsCalculatingTokens] = useState(false); + const tokenDebounceTimerRef = useRef | null>(null); + + useEffect(() => { + if (tokenDebounceTimerRef.current) { + clearTimeout(tokenDebounceTimerRef.current); + tokenDebounceTimerRef.current = null; + } + + if (input.length === 0) { + setTokenCount(0); + setIsCalculatingTokens(false); + return; + } + + setIsCalculatingTokens(true); + + tokenDebounceTimerRef.current = setTimeout(() => { + const tokens = roughTokenCountEstimation(input); + setTokenCount(tokens); + setIsCalculatingTokens(false); + tokenDebounceTimerRef.current = null; + }, 300); + + return () => { + if (tokenDebounceTimerRef.current) { + clearTimeout(tokenDebounceTimerRef.current); + } + }; + }, [input]); + + useEffect(() => { + return () => { + if (tokenDebounceTimerRef.current) { + clearTimeout(tokenDebounceTimerRef.current); + } + }; + }, []); // Track the last input value set via internal handlers so we can detect // external input changes (e.g. speech-to-text injection) and move cursor to end. const lastInternalInputRef = React.useRef(input); @@ -2271,7 +2311,7 @@ function PromptInput({ {textInputElement} } - 0} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} bridgeSelected={bridgeSelected} tmuxSelected={tmuxSelected} teammateFooterIndex={teammateFooterIndex} ideSelection={ideSelection} mcpClients={mcpClients} isPasting={isPasting} isInputWrapped={isInputWrapped} messages={messages} isSearching={isSearchingHistory} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined} /> + 0} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} bridgeSelected={bridgeSelected} tmuxSelected={tmuxSelected} teammateFooterIndex={teammateFooterIndex} ideSelection={ideSelection} mcpClients={mcpClients} isPasting={isPasting} isInputWrapped={isInputWrapped} messages={messages} isSearching={isSearchingHistory} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined} tokenCount={tokenCount} isCalculatingTokens={isCalculatingTokens} /> {isFullscreenEnvEnabled() ? null : autoModeOptInDialog} {isFullscreenEnvEnabled() ? // position=absolute takes zero layout height so the spinner diff --git a/src/components/PromptInput/PromptInputFooter.tsx b/src/components/PromptInput/PromptInputFooter.tsx index e881ddb7..a0523467 100644 --- a/src/components/PromptInput/PromptInputFooter.tsx +++ b/src/components/PromptInput/PromptInputFooter.tsx @@ -59,6 +59,8 @@ type Props = { setHistoryQuery: (query: string) => void; historyFailedMatch: boolean; onOpenTasksDialog?: (taskId?: string) => void; + tokenCount: number; + isCalculatingTokens: boolean; }; function PromptInputFooter({ apiKeyStatus, @@ -92,7 +94,9 @@ function PromptInputFooter({ historyQuery, setHistoryQuery, historyFailedMatch, - onOpenTasksDialog + onOpenTasksDialog, + tokenCount, + isCalculatingTokens }: Props): ReactNode { const settings = useSettings(); const { @@ -142,6 +146,9 @@ function PromptInputFooter({ + + {isCalculatingTokens ? '...' : `${tokenCount} tokens`} + {isFullscreen ? null : } {"external" === 'ant' && isUndercover() && undercover} From c1ef419315de1ecaf16b7665b09b6ac1126267d7 Mon Sep 17 00:00:00 2001 From: ouyu <1986834078@qq.com> Date: Thu, 16 Apr 2026 20:08:21 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(PromptInput):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E7=B2=98=E8=B4=B4=E5=86=85=E5=AE=B9=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E4=BB=A4=E7=89=8C=E8=AE=A1=E7=AE=97=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入新的令牌计算逻辑,处理包含引用的文本和图片内容。当文本中包含图片引用时,自动增加2000个令牌的估算值,以更准确地反映实际令牌消耗。 --- src/components/PromptInput/PromptInput.tsx | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index d916a5dc..a00d6100 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -120,6 +120,7 @@ import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'; import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'; import { useShowFastIconHint } from './useShowFastIconHint.js'; import { useSwarmBanner } from './useSwarmBanner.js'; +import { expandPastedTextRefs, parseReferences } from '../../history.js'; import { roughTokenCountEstimation } from '../../services/tokenEstimation.js'; import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'; type Props = { @@ -251,7 +252,25 @@ function PromptInput({ show: false }); const [cursorOffset, setCursorOffset] = useState(input.length); - const [tokenCount, setTokenCount] = useState(input.length === 0 ? 0 : roughTokenCountEstimation(input)); + + const calculateTotalTokens = useCallback((text: string, contents: Record): number => { + const expandedText = expandPastedTextRefs(text, contents); + let tokens = roughTokenCountEstimation(expandedText); + + const refs = parseReferences(text); + for (const ref of refs) { + const content = contents[ref.id]; + if (content?.type === 'image') { + tokens += 2000; + } + } + + return tokens; + }, []); + + const [tokenCount, setTokenCount] = useState( + input.length === 0 ? 0 : calculateTotalTokens(input, pastedContents) + ); const [isCalculatingTokens, setIsCalculatingTokens] = useState(false); const tokenDebounceTimerRef = useRef | null>(null); @@ -270,7 +289,7 @@ function PromptInput({ setIsCalculatingTokens(true); tokenDebounceTimerRef.current = setTimeout(() => { - const tokens = roughTokenCountEstimation(input); + const tokens = calculateTotalTokens(input, pastedContents); setTokenCount(tokens); setIsCalculatingTokens(false); tokenDebounceTimerRef.current = null; @@ -281,7 +300,7 @@ function PromptInput({ clearTimeout(tokenDebounceTimerRef.current); } }; - }, [input]); + }, [input, pastedContents, calculateTotalTokens]); useEffect(() => { return () => {