From e92fcbab46c92aa99615abb5b22a657964209c0d Mon Sep 17 00:00:00 2001 From: pomelo Date: Wed, 1 Jul 2026 06:44:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20switch=20TUI=20prefix=20=E2=9C=A6?= =?UTF-8?q?=E2=86=92=E2=97=86=20to=20fix=20glyph=20overflow=20on=20some=20?= =?UTF-8?q?terminals=20(#5974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 --------- Co-authored-by: Qwen Code Co-authored-by: pomelo.lcw Co-authored-by: Qwen-Coder --- docs/users/features/memory.md | 2 +- integration-tests/interactive/cron-interactive.test.ts | 2 +- .../terminal-capture/scenarios/message-components.ts | 2 +- packages/cli/src/ui/components/Footer.tsx | 2 +- packages/cli/src/ui/components/HistoryItemDisplay.test.tsx | 2 +- packages/cli/src/ui/components/VoiceIndicator.tsx | 4 ++-- .../__snapshots__/HistoryItemDisplay.test.tsx.snap | 4 ++-- .../src/ui/components/messages/CompressionMessage.test.tsx | 6 +++--- .../cli/src/ui/components/messages/CompressionMessage.tsx | 2 +- .../src/ui/components/messages/ConversationMessages.tsx | 7 ++++--- .../cli/src/ui/components/shared/ToolStatusIndicator.tsx | 2 +- .../packages/ui/src/components/chat/UserMessageBubble.tsx | 2 +- 12 files changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/users/features/memory.md b/docs/users/features/memory.md index 8da31b2865..1e794e1917 100644 --- a/docs/users/features/memory.md +++ b/docs/users/features/memory.md @@ -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 diff --git a/integration-tests/interactive/cron-interactive.test.ts b/integration-tests/interactive/cron-interactive.test.ts index d0dd3711b1..7739e86342 100644 --- a/integration-tests/interactive/cron-interactive.test.ts +++ b/integration-tests/interactive/cron-interactive.test.ts @@ -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 () => { diff --git a/integration-tests/terminal-capture/scenarios/message-components.ts b/integration-tests/terminal-capture/scenarios/message-components.ts index 621eb1ef82..9d8923e0b2 100644 --- a/integration-tests/terminal-capture/scenarios/message-components.ts +++ b/integration-tests/terminal-capture/scenarios/message-components.ts @@ -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', diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 020aae41bd..a2a9ab3677 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -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) { diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index 827c54c3ed..8ae260ca58 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -81,7 +81,7 @@ describe('', () => { 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', () => { diff --git a/packages/cli/src/ui/components/VoiceIndicator.tsx b/packages/cli/src/ui/components/VoiceIndicator.tsx index 387cac398a..9f90ae66d4 100644 --- a/packages/cli/src/ui/components/VoiceIndicator.tsx +++ b/packages/cli/src/ui/components/VoiceIndicator.tsx @@ -48,9 +48,9 @@ export function VoiceIndicator({ {' ' + t('listening…')} ) : status === 'refining' ? ( - {'✦ ' + t('refining…')} + {'◆ ' + t('refining…')} ) : ( - {'✦ ' + t('transcribing…')} + {'◆ ' + t('transcribing…')} )} {interimText ? ( diff --git a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap index c58c38dcab..fef24b7528 100644 --- a/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/HistoryItemDisplay.test.tsx.snap @@ -2,7 +2,7 @@ exports[` > 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[` > should render a full gemini_content item when exports[` > should render a truncated gemini item 1`] = ` " - ✦ Example code block: + ◆ Example code block: ... first 41 lines hidden ... 42 Line 42 43 Line 43 diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx index d648cb06b7..b121fb738d 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx @@ -45,7 +45,7 @@ describe('', () => { const { lastFrame } = render(); 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('', () => { const { lastFrame } = render(); const output = lastFrame(); - expect(output).toContain('✦'); + expect(output).toContain('◆'); expect(output).toContain( `compressed from ${original} to ${newTokens} tokens`, ); @@ -89,7 +89,7 @@ describe('', () => { const { lastFrame } = render(); const output = lastFrame(); - expect(output).toContain('✦'); + expect(output).toContain('◆'); expect(output).toContain( 'Compression was not beneficial for this history size.', ); diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx index 362b9e052e..771fa43c72 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx @@ -72,7 +72,7 @@ export function CompressionMessage({ {isPending ? ( ) : ( - + )} diff --git a/packages/cli/src/ui/components/messages/ConversationMessages.tsx b/packages/cli/src/ui/components/messages/ConversationMessages.tsx index 2b1093f317..e7fca97eaa 100644 --- a/packages/cli/src/ui/components/messages/ConversationMessages.tsx +++ b/packages/cli/src/ui/components/messages/ConversationMessages.tsx @@ -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 = ({ }) => ( ); @@ -321,7 +322,7 @@ export const ThinkMessage: React.FC = ({ return ( - {THINKING_ICON} + {THINKING_ICON_PENDING} {t('Thinking')}…{durationSuffix} diff --git a/packages/cli/src/ui/components/shared/ToolStatusIndicator.tsx b/packages/cli/src/ui/components/shared/ToolStatusIndicator.tsx index 51ddfc2be6..555f5de1f2 100644 --- a/packages/cli/src/ui/components/shared/ToolStatusIndicator.tsx +++ b/packages/cli/src/ui/components/shared/ToolStatusIndicator.tsx @@ -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; diff --git a/packages/desktop/packages/ui/src/components/chat/UserMessageBubble.tsx b/packages/desktop/packages/ui/src/components/chat/UserMessageBubble.tsx index 01f88de919..a12d58e30d 100644 --- a/packages/desktop/packages/ui/src/components/chat/UserMessageBubble.tsx +++ b/packages/desktop/packages/ui/src/components/chat/UserMessageBubble.tsx @@ -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 = '/'