mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 19:52:02 +00:00
fix(cli): add TUI flicker foundation fixes (#3591)
* fix(cli): reduce main screen flicker * fix(cli): pre-slice large tool text output * fix(cli): slice tool output by visual height * fix(core): preserve shell transcript across narrow wraps * fix(core): suppress soft-wrap-only shell rerenders * fix(core): compare default shell output by logical wraps * fix(cli): gate synchronized terminal output --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
This commit is contained in:
parent
007a109db8
commit
54465b0c02
15 changed files with 1473 additions and 200 deletions
|
|
@ -547,6 +547,76 @@ describe('<ToolMessage />', () => {
|
|||
expect(output).toContain('line 30');
|
||||
});
|
||||
|
||||
it('pre-slices large non-shell string output before MaxSizedBox layout', () => {
|
||||
const longString = Array.from(
|
||||
{ length: 5000 },
|
||||
(_, i) => `line ${i + 1}`,
|
||||
).join('\n');
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
name="some-other-tool"
|
||||
resultDisplay={longString}
|
||||
status={ToolCallStatus.Success}
|
||||
availableTerminalHeight={12}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
const output = lastFrame()!;
|
||||
|
||||
expect(output).toContain('... first 4995 lines hidden ...');
|
||||
expect(output).not.toContain('line 4995');
|
||||
expect(output).toContain('line 4996');
|
||||
expect(output).toContain('line 4997');
|
||||
expect(output).toContain('line 4998');
|
||||
expect(output).toContain('line 4999');
|
||||
expect(output).toContain('line 5000');
|
||||
});
|
||||
|
||||
it('pre-slices single-line output by visual width before MaxSizedBox layout', () => {
|
||||
const longSingleLine = Array.from({ length: 1000 }, (_, i) =>
|
||||
String(i % 10),
|
||||
).join('');
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
name="some-other-tool"
|
||||
contentWidth={20}
|
||||
resultDisplay={longSingleLine}
|
||||
status={ToolCallStatus.Success}
|
||||
availableTerminalHeight={12}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
const output = lastFrame()!;
|
||||
|
||||
expect(output).toMatch(/\.\.\. first \d+ lin/);
|
||||
expect(output).not.toContain(longSingleLine);
|
||||
expect(output).toContain(longSingleLine.slice(-10));
|
||||
});
|
||||
|
||||
it('does not pre-slice string output that exactly fits available height', () => {
|
||||
const exactFitString = Array.from(
|
||||
{ length: 6 },
|
||||
(_, i) => `line ${i + 1}`,
|
||||
).join('\n');
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
name="some-other-tool"
|
||||
resultDisplay={exactFitString}
|
||||
status={ToolCallStatus.Success}
|
||||
availableTerminalHeight={12}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
const output = lastFrame()!;
|
||||
|
||||
expect(output).not.toContain('lines hidden');
|
||||
expect(output).toContain('line 1');
|
||||
expect(output).toContain('line 6');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['negative', -1],
|
||||
['fractional', 1.5],
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { DiffRenderer } from './DiffRenderer.js';
|
|||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText, ShellStatsBar } from '../AnsiOutput.js';
|
||||
import type { ShellStatsBarProps } from '../AnsiOutput.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
|
||||
import { TodoDisplay } from '../TodoDisplay.js';
|
||||
import type {
|
||||
TodoResultDisplay,
|
||||
|
|
@ -31,6 +31,7 @@ import { theme } from '../../semantic-colors.js';
|
|||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../../config/settings.js';
|
||||
import { useCompactMode } from '../../contexts/CompactModeContext.js';
|
||||
import { getCachedStringWidth, toCodePoints } from '../../utils/textUtils.js';
|
||||
|
||||
import {
|
||||
ToolStatusIndicator,
|
||||
|
|
@ -48,6 +49,65 @@ const DEFAULT_SHELL_OUTPUT_MAX_LINES = 5;
|
|||
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
|
||||
export type TextEmphasis = 'high' | 'medium' | 'low';
|
||||
|
||||
function sliceTextForMaxHeight(
|
||||
text: string,
|
||||
maxHeight: number | undefined,
|
||||
maxWidth: number,
|
||||
): { text: string; hiddenLinesCount: number } {
|
||||
if (maxHeight === undefined) {
|
||||
return { text, hiddenLinesCount: 0 };
|
||||
}
|
||||
|
||||
const targetMaxHeight = Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT);
|
||||
const visibleContentHeight = targetMaxHeight - 1;
|
||||
const visualWidth = Math.max(1, Math.floor(maxWidth));
|
||||
const visibleLines: string[] = [];
|
||||
let visualLineCount = 0;
|
||||
let currentLine = '';
|
||||
let currentLineWidth = 0;
|
||||
|
||||
const appendVisibleLine = (line: string) => {
|
||||
visualLineCount += 1;
|
||||
visibleLines.push(line);
|
||||
if (visibleLines.length > visibleContentHeight) {
|
||||
visibleLines.shift();
|
||||
}
|
||||
};
|
||||
|
||||
const flushCurrentLine = () => {
|
||||
appendVisibleLine(currentLine);
|
||||
currentLine = '';
|
||||
currentLineWidth = 0;
|
||||
};
|
||||
|
||||
for (const char of toCodePoints(text)) {
|
||||
if (char === '\n') {
|
||||
flushCurrentLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
const charWidth = Math.max(getCachedStringWidth(char), 1);
|
||||
if (currentLineWidth > 0 && currentLineWidth + charWidth > visualWidth) {
|
||||
flushCurrentLine();
|
||||
}
|
||||
|
||||
currentLine += char;
|
||||
currentLineWidth += charWidth;
|
||||
}
|
||||
|
||||
flushCurrentLine();
|
||||
|
||||
if (visualLineCount <= targetMaxHeight) {
|
||||
return { text, hiddenLinesCount: 0 };
|
||||
}
|
||||
|
||||
const hiddenLinesCount = visualLineCount - visibleContentHeight;
|
||||
return {
|
||||
text: visibleLines.join('\n'),
|
||||
hiddenLinesCount,
|
||||
};
|
||||
}
|
||||
|
||||
type DisplayRendererResult =
|
||||
| { type: 'none' }
|
||||
| { type: 'todo'; data: TodoResultDisplay }
|
||||
|
|
@ -234,11 +294,21 @@ const StringResultRenderer: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
const sliced = sliceTextForMaxHeight(
|
||||
displayData,
|
||||
availableHeight,
|
||||
childWidth,
|
||||
);
|
||||
|
||||
return (
|
||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||
<MaxSizedBox
|
||||
maxHeight={availableHeight}
|
||||
maxWidth={childWidth}
|
||||
additionalHiddenLinesCount={sliced.hiddenLinesCount}
|
||||
>
|
||||
<Box>
|
||||
<Text wrap="wrap" color={theme.text.primary}>
|
||||
{displayData}
|
||||
{sliced.text}
|
||||
</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue