Merge pull request #2776 from QwenLM/feat/enhance-btw-command

feat(cli): enhance /btw side question with improved prompt and Ctrl+C/D cancel
This commit is contained in:
Shaojin Wen 2026-04-03 19:17:12 +08:00 committed by GitHub
parent 61bc80fe19
commit e855229453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 351 additions and 57 deletions

View file

@ -5,7 +5,7 @@
*/
import { describe, expect, it } from 'vitest';
import { render } from 'ink-testing-library';
import { renderWithProviders } from '../../../test-utils/render.js';
import { BtwMessage } from './BtwMessage.js';
describe('BtwMessage', () => {
@ -16,7 +16,7 @@ describe('BtwMessage', () => {
});
it('renders the side question and answer', () => {
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<BtwMessage
btw={{
question: 'side question',
@ -31,4 +31,57 @@ describe('BtwMessage', () => {
expect(output).toContain('side question');
expect(output).toContain('side answer');
});
it('renders pending state with cancel hint', () => {
const { lastFrame } = renderWithProviders(
<BtwMessage
btw={{
question: 'pending question',
answer: '',
isPending: true,
}}
/>,
);
const output = lastFrame() ?? '';
expect(output).toContain('/btw');
expect(output).toContain('pending question');
expect(output).toContain('Answering...');
expect(output).toContain('Ctrl+C');
expect(output).toContain('Ctrl+D');
});
it('accepts containerWidth prop for content width calculation', () => {
const { lastFrame } = renderWithProviders(
<BtwMessage
btw={{
question: 'q',
answer: 'some answer text',
isPending: false,
}}
containerWidth={60}
/>,
);
const output = lastFrame() ?? '';
expect(output).toContain('some answer text');
});
it('renders dismiss hint when answer is complete', () => {
const { lastFrame } = renderWithProviders(
<BtwMessage
btw={{
question: 'q',
answer: 'a',
isPending: false,
}}
/>,
);
const output = lastFrame() ?? '';
expect(output).toContain('Space');
expect(output).toContain('Enter');
expect(output).toContain('Escape');
expect(output).toContain('dismiss');
});
});

View file

@ -9,46 +9,80 @@ import { Box, Text } from 'ink';
import type { BtwProps } from '../../types.js';
import { Colors } from '../../colors.js';
import { t } from '../../../i18n/index.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
export interface BtwDisplayProps {
btw: BtwProps;
/** Width of the parent container. Used to compute Markdown content width.
* Falls back to terminal width when not provided. */
containerWidth?: number;
}
const BtwMessageInternal: React.FC<BtwDisplayProps> = ({ btw }) => (
<Box
flexDirection="column"
borderStyle="round"
borderColor={Colors.AccentYellow}
paddingX={1}
width="100%"
>
<Box flexDirection="row">
<Text color={Colors.AccentYellow} bold>
{'/btw '}
</Text>
<Text wrap="wrap" color={Colors.AccentYellow}>
{btw.question}
</Text>
// border(1)*2 + paddingX(1)*2 = 4
const BTW_SELF_CHROME = 4;
/**
* Ensure code fences (``` or ~~~) start on their own line so that
* MarkdownDisplay's line-based parser can detect them. Models sometimes
* emit the opening fence right after prose text without a preceding newline.
*/
function normalizeCodeFences(text: string): string {
return text.replace(/([^\n])(```|~~~)/g, '$1\n$2');
}
const BtwMessageInternal: React.FC<BtwDisplayProps> = ({
btw,
containerWidth,
}) => {
const { columns: terminalWidth } = useTerminalSize();
const baseWidth = containerWidth ?? terminalWidth;
const contentWidth = Math.max(2, baseWidth - BTW_SELF_CHROME);
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={Colors.AccentYellow}
paddingX={1}
width="100%"
>
<Box flexDirection="row">
<Text color={Colors.AccentYellow} bold>
{'/btw '}
</Text>
<Text wrap="wrap" color={Colors.AccentYellow}>
{btw.question}
</Text>
</Box>
{btw.isPending ? (
<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, Ctrl+C, or Ctrl+D to cancel')}
</Text>
</Box>
</Box>
) : (
<Box flexDirection="column" marginTop={1}>
<MarkdownDisplay
text={normalizeCodeFences(btw.answer)}
isPending={false}
contentWidth={contentWidth}
/>
<Box marginTop={1}>
<Text dimColor>
{t('Press Space, Enter, or Escape to dismiss')}
</Text>
</Box>
</Box>
)}
</Box>
{btw.isPending ? (
<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" marginTop={1}>
<Text wrap="wrap">{btw.answer}</Text>
<Box marginTop={1}>
<Text dimColor>{t('Press Space, Enter, or Escape to dismiss')}</Text>
</Box>
</Box>
)}
</Box>
);
);
};
export const BtwMessage = React.memo(BtwMessageInternal);