fix(cli): improve /btw overlay UX — layout, dismiss hints, and history cleanup

- Make /btw overlay mutually exclusive with Composer (replaces input area)
- Add dismiss hints: "Press Escape to cancel" (pending) / "Press Space,
  Enter, or Escape to dismiss" (completed)
- Skip adding /btw to conversation history to avoid duplicate display
- Prioritize dialog shortcuts over btw dismiss via dialogsVisibleRef
- Add `sleep` property to terminal-capture FlowStep for async wait scenarios

Made-with: Cursor
This commit is contained in:
yiliang114 2026-03-21 01:07:02 +08:00
parent 130d6888b4
commit dff9822f9b
6 changed files with 59 additions and 23 deletions

View file

@ -958,6 +958,7 @@ export const AppContainer = (props: AppContainerProps) => {
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
const [escapePressedOnce, setEscapePressedOnce] = useState(false);
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
const dialogsVisibleRef = useRef(false);
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [ideContextState, setIdeContextState] = useState<
IdeContext | undefined
@ -1244,8 +1245,9 @@ export const AppContainer = (props: AppContainerProps) => {
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
return;
} else if (keyMatchers[Command.ESCAPE](key)) {
// Dismiss or cancel btw side-question on Escape
if (btwItem) {
// Dismiss or cancel btw side-question on Escape,
// but only when btw is actually visible (not hidden behind a dialog).
if (btwItem && !dialogsVisibleRef.current) {
cancelBtw();
return;
}
@ -1292,8 +1294,13 @@ export const AppContainer = (props: AppContainerProps) => {
}
// Dismiss completed btw side-question on Space or Enter,
// but only when the input buffer is empty so we don't swallow user keystrokes.
if (btwItem && !btwItem.btw.isPending && buffer.text.length === 0) {
// but only when btw is visible and the input buffer is empty.
if (
btwItem &&
!btwItem.btw.isPending &&
!dialogsVisibleRef.current &&
buffer.text.length === 0
) {
if (key.name === 'return' || key.sequence === ' ') {
setBtwItem(null);
return;
@ -1430,6 +1437,7 @@ export const AppContainer = (props: AppContainerProps) => {
isApprovalModeDialogOpen ||
isResumeDialogOpen ||
isExtensionsManagerDialogOpen;
dialogsVisibleRef.current = dialogsVisible;
const {
isFeedbackDialogOpen,

View file

@ -31,12 +31,17 @@ const BtwMessageInternal: React.FC<BtwDisplayProps> = ({ btw }) => (
</Text>
</Box>
{btw.isPending ? (
<Box>
<Text color={Colors.AccentYellow}>{'+ '}</Text>
<Text color={Colors.AccentYellow}>{t('Answering...')}</Text>
<Box flexDirection="column" marginTop={1}>
<Box>
<Text color={Colors.AccentYellow}>{'+ '}</Text>
<Text color={Colors.AccentYellow}>{t('Answering...')}</Text>
</Box>
<Box marginTop={1}>
<Text dimColor>{t('Press Escape to cancel')}</Text>
</Box>
</Box>
) : (
<Box flexDirection="column">
<Box flexDirection="column" marginTop={1}>
<Text wrap="wrap">{btw.answer}</Text>
<Box marginTop={1}>
<Text dimColor>{t('Press Space, Enter, or Escape to dismiss')}</Text>

View file

@ -37,6 +37,7 @@ import { BundledSkillLoader } from '../../services/BundledSkillLoader.js';
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
import { parseSlashCommand } from '../../utils/commands.js';
import { isBtwCommand } from '../utils/commandUtils.js';
import { clearScreen } from '../../utils/stdioHelpers.js';
import { useKeypress } from './useKeypress.js';
import {
@ -385,10 +386,12 @@ export const useSlashCommandProcessor = (
abortControllerRef.current = abortController;
const userMessageTimestamp = Date.now();
addItemWithRecording(
{ type: MessageType.USER, text: trimmed },
userMessageTimestamp,
);
if (!isBtwCommand(trimmed)) {
addItemWithRecording(
{ type: MessageType.USER, text: trimmed },
userMessageTimestamp,
);
}
let hasError = false;
const {

View file

@ -55,11 +55,6 @@ export const DefaultAppLayout: React.FC = () => {
<>
{/* Main view: conversation history + main composer / dialogs */}
<MainContent />
{uiState.btwItem && (
<Box marginX={2} width={uiState.mainAreaWidth}>
<BtwMessage btw={uiState.btwItem.btw} />
</Box>
)}
<Box flexDirection="column" ref={uiState.mainControlsRef}>
{uiState.dialogsVisible ? (
<Box
@ -72,6 +67,10 @@ export const DefaultAppLayout: React.FC = () => {
addItem={uiState.historyManager.addItem}
/>
</Box>
) : uiState.btwItem ? (
<Box marginX={2} width={terminalWidth - 4}>
<BtwMessage btw={uiState.btwItem.btw} />
</Box>
) : (
<Composer />
)}

View file

@ -26,12 +26,6 @@ export const ScreenReaderAppLayout: React.FC = () => {
<MainContent />
</Box>
{uiState.btwItem && (
<Box marginX={2} width={uiState.mainAreaWidth}>
<BtwMessage btw={uiState.btwItem.btw} />
</Box>
)}
{uiState.dialogsVisible ? (
<Box marginX={2} flexDirection="column" width={uiState.mainAreaWidth}>
<DialogManager
@ -39,6 +33,10 @@ export const ScreenReaderAppLayout: React.FC = () => {
addItem={uiState.historyManager.addItem}
/>
</Box>
) : uiState.btwItem ? (
<Box marginX={2} width={uiState.terminalWidth - 4}>
<BtwMessage btw={uiState.btwItem.btw} />
</Box>
) : (
<Composer />
)}