fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some terminals (#5974)

* fix(cli): replace ✦ (U+2726) with ◆ (U+25C6) and add ∵/∴ thinking icons

- Replace ✦ with ◆ across all TUI components to fix East Asian
  Ambiguous width misalignment (string-width reports 1 but terminals
  render 2 columns).
- Use ∵ (because) during thinking streaming, ∴ (therefore) when
  thinking is complete — matches the mathematical reasoning pair.

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>

* fix(cli): reduce STATUS_INDICATOR_WIDTH from 3 to 2 after ◆ replacement

◆ (U+25C6) is a consistent width-1 character across all terminals,
so the tool status indicator no longer needs the extra column that
was reserved for the ambiguous-width ✦ (U+2726).

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>

* fix(cli): catch missed ✦→◆ references in tests, docs, and scenarios

* fix(cli): shorten tmux spinner frames from 3 to 2 chars to match STATUS_INDICATOR_WIDTH=2

TMUX_SPINNER_FRAMES changed from ['.  ', '.. ', '...'] to ['· ', '··']
to prevent 1-column overflow in tmux when STATUS_INDICATOR_WIDTH was
reduced from 3 to 2 after the ◆ replacement.

* revert(cli): keep narrow '.' tmux spinner frames instead of ambiguous '·'

'.' (U+002E) is Narrow (always 1 col), giving a guaranteed fixed-width
tmux spinner. '·' (U+00B7) is East Asian Ambiguous, so on ambiguous-width=2
terminals the frames become 3/4 cols and the spinner jitters — the opposite
of the "fixed-width frames" the surrounding comment promises.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen Code <noreply@alibaba-inc.com>
Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
pomelo 2026-07-01 06:44:34 +08:00 committed by GitHub
parent c48dc32b57
commit e92fcbab46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 19 additions and 18 deletions

View file

@ -99,7 +99,7 @@ Everything saved is plain markdown — you can open, edit, or delete any file at
Qwen periodically goes through its saved memories to remove duplicates and clean up outdated entries. This runs automatically in the background once a day after enough sessions have accumulated. You can trigger it manually with `/dream` if you want it to run now.
While cleanup is running, **✦ dreaming** appears in the corner of the screen. Your session continues normally.
Your session continues normally while cleanup runs in the background.
### Turning it on or off

View file

@ -66,7 +66,7 @@ function makeEnv(): NodeJS.ProcessEnv {
const afterPrompt = finalScreen.slice(
finalScreen.lastIndexOf('Cron: PONG7742'),
);
expect(afterPrompt).toContain('');
expect(afterPrompt).toContain('');
});
it('user input takes priority over cron', { timeout: 180_000 }, async () => {

View file

@ -8,7 +8,7 @@ import type { ScenarioConfig } from '../scenario-runner.js';
* - Info message prefix ( filled circle)
* - Error message prefix ()
* - User message prefix (>)
* - Assistant message prefix ()
* - Assistant message prefix ()
*/
export default {
name: 'message-components',

View file

@ -124,7 +124,7 @@ export const Footer: React.FC = () => {
});
}
// Dream tasks now surface via the BackgroundTasksPill (e.g. "1 dream")
// alongside the other background-task kinds. The previous ` dreaming`
// alongside the other background-task kinds. The previous ` dreaming`
// right-column indicator was removed to avoid two simultaneous signals
// for the same underlying state.
if (promptTokenCount > 0 && contextWindowSize && !hideContextIndicator) {

View file

@ -81,7 +81,7 @@ describe('<HistoryItemDisplay />', () => {
const output = lastFrame() ?? '';
expect(output.startsWith('\n')).toBe(true);
expect(output).toContain(' Hello');
expect(output).toContain(' Hello');
});
it('renders tool summaries without a leading spacer row', () => {

View file

@ -48,9 +48,9 @@ export function VoiceIndicator({
<Text color="gray">{' ' + t('listening…')}</Text>
</>
) : status === 'refining' ? (
<Text color="yellow">{' ' + t('refining…')}</Text>
<Text color="yellow">{' ' + t('refining…')}</Text>
) : (
<Text color="yellow">{' ' + t('transcribing…')}</Text>
<Text color="yellow">{' ' + t('transcribing…')}</Text>
)}
</Box>
{interimText ? (

View file

@ -2,7 +2,7 @@
exports[`<HistoryItemDisplay /> > should render a full gemini item when using availableTerminalHeightGemini 1`] = `
"
Example code block:
Example code block:
1 Line 1
2 Line 2
3 Line 3
@ -111,7 +111,7 @@ exports[`<HistoryItemDisplay /> > should render a full gemini_content item when
exports[`<HistoryItemDisplay /> > should render a truncated gemini item 1`] = `
"
Example code block:
Example code block:
... first 41 lines hidden ...
42 Line 42
43 Line 43

View file

@ -45,7 +45,7 @@ describe('<CompressionMessage />', () => {
const { lastFrame } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('');
expect(output).toContain('');
expect(output).toContain(
'Chat history compressed from 100 to 50 tokens.',
);
@ -67,7 +67,7 @@ describe('<CompressionMessage />', () => {
const { lastFrame } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('');
expect(output).toContain('');
expect(output).toContain(
`compressed from ${original} to ${newTokens} tokens`,
);
@ -89,7 +89,7 @@ describe('<CompressionMessage />', () => {
const { lastFrame } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('');
expect(output).toContain('');
expect(output).toContain(
'Compression was not beneficial for this history size.',
);

View file

@ -72,7 +72,7 @@ export function CompressionMessage({
{isPending ? (
<Spinner type="dots" />
) : (
<Text color={theme.text.accent}></Text>
<Text color={theme.text.accent}></Text>
)}
</Box>
<Box>

View file

@ -21,6 +21,7 @@ import { wrapToVisualLines } from '../../utils/textUtils.js';
import { formatDuration } from '../../utils/displayUtils.js';
export const THINKING_ICON = '∴ ';
export const THINKING_ICON_PENDING = '∵ ';
export const toggleKeyHint =
process.platform === 'darwin' ? 'option+t' : 'alt+t';
@ -233,7 +234,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
}) => (
<PrefixedMarkdownMessage
text={text}
prefix=""
prefix=""
prefixColor={theme.text.accent}
ariaLabel={SCREEN_READER_MODEL_PREFIX}
isPending={isPending}
@ -257,7 +258,7 @@ export const AssistantMessageContent: React.FC<
isPending={isPending}
availableTerminalHeight={availableTerminalHeight}
contentWidth={contentWidth}
basePrefix=""
basePrefix=""
sourceCopyIndexOffsets={sourceCopyIndexOffsets}
/>
);
@ -321,7 +322,7 @@ export const ThinkMessage: React.FC<ThinkMessageProps> = ({
return (
<Box flexDirection="column">
<Text dimColor italic>
{THINKING_ICON}
{THINKING_ICON_PENDING}
{t('Thinking')}{durationSuffix}
</Text>
<Box paddingLeft={2}>

View file

@ -15,7 +15,7 @@ import {
} from '../../constants.js';
import { theme } from '../../semantic-colors.js';
export const STATUS_INDICATOR_WIDTH = 3;
export const STATUS_INDICATOR_WIDTH = 2;
type ToolStatusIndicatorProps = {
status: ToolCallStatus;

View file

@ -24,7 +24,7 @@ import { useTranslation } from 'react-i18next'
// Fallback text icons for badges without iconDataUrl
// Using simple characters since SVG rendering may not work in all contexts
const SKILL_ICON_TEXT = ''
const SKILL_ICON_TEXT = ''
const SOURCE_ICON_TEXT = '⊕'
const CONTEXT_ICON_TEXT = '⚙'
const COMMAND_ICON_TEXT = '/'