From 67105127cd75f05ca78912579339e979e8dbc98c Mon Sep 17 00:00:00 2001 From: hcyang Date: Fri, 5 Jun 2026 15:52:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E4=BC=98=E5=8C=96=20PromptInput=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=E5=85=89=E6=A0=87=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=8E=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将光标前缀包装在固定宽度的 Box 中,防止布局抖动 - 通过 getPromptCursorPlacement 计算光标位置,实现光标的精准定位 - 新增 usePromptTerminalCursor 钩子管理光标渲染 - 调整输入区宽度适配屏幕宽度,避免溢出 - 移除重复的终端焦点与光标隐藏钩子调用,优化副作用管理 - 统一控制终端光标显示,隐藏系统光标防止视觉冲突 --- src/ui/views/PromptInput.tsx | 39 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 19342da5..48eb659a 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -43,7 +43,13 @@ import { } from "../core/file-mentions"; import type { FileMentionItem } from "../core/file-mentions"; import { readClipboardImageAsync } from "../core/clipboard"; -import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; +import { + useTerminalInput, + usePasteHandling, + useHistoryNavigation, + getPromptCursorPlacement, + usePromptTerminalCursor, +} from "../hooks"; import type { InputKey } from "../hooks"; import { useHiddenTerminalCursor, @@ -108,7 +114,11 @@ const PromptPrefixLine = React.memo(function PromptPrefixLine({ busy }: { busy: }, [busy]); const prefix = busy ? `${SPINNER_FRAMES[spinnerIndex]} ` : "> "; - return {prefix}; + return ( + + {prefix} + + ); }); export const PromptInput = React.memo(function PromptInput({ @@ -202,9 +212,17 @@ export const PromptInput = React.memo(function PromptInput({ () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] ); - // The prompt draws its own inverse-video cursor inside the text. Keep the - // native terminal cursor hidden so wrapping edges do not show two cursors. - const hideNativeCursor = !disabled; + + const cursorPlacement = useMemo( + () => getPromptCursorPlacement(buffer, screenWidth, 2, footerText), + [buffer, footerText, screenWidth] + ); + const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText; + useTerminalFocusReporting(stdout, !disabled); + useTerminalExtendedKeys(stdout, !disabled); + useBracketedPaste(stdout, !disabled); + usePromptTerminalCursor(stdout, cursorPlacement, usePositionedCursor); + useHiddenTerminalCursor(stdout, !disabled && !usePositionedCursor); const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -560,10 +578,6 @@ export const PromptInput = React.memo(function PromptInput({ }, { isActive: !disabled } ); - useTerminalFocusReporting(stdout, !disabled); - useTerminalExtendedKeys(stdout, !disabled); - useBracketedPaste(stdout, !disabled); - useHiddenTerminalCursor(stdout, hideNativeCursor); function undo(): void { const previous = undoPromptEdit(undoRedoRef.current, buffer); @@ -742,6 +756,7 @@ export const PromptInput = React.memo(function PromptInput({ ) : null} {/* Input */} - {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} - {inlineHint ? {inlineHint} : null} + + {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} + {inlineHint ? {inlineHint} : null} +