mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 12:11:09 +00:00
Merge branch 'main' into feat/shell-pty-default-and-enhancements
This commit is contained in:
commit
ca3a2be2ec
130 changed files with 16089 additions and 2249 deletions
|
|
@ -8,19 +8,23 @@ import type React from 'react';
|
|||
import { useMemo } from 'react';
|
||||
import { escapeAnsiCtrlCodes } from '../utils/textUtils.js';
|
||||
import type { HistoryItem } from '../types.js';
|
||||
import { UserMessage } from './messages/UserMessage.js';
|
||||
import { UserShellMessage } from './messages/UserShellMessage.js';
|
||||
import { GeminiMessage } from './messages/GeminiMessage.js';
|
||||
import { InfoMessage } from './messages/InfoMessage.js';
|
||||
import { ErrorMessage } from './messages/ErrorMessage.js';
|
||||
import {
|
||||
UserMessage,
|
||||
UserShellMessage,
|
||||
AssistantMessage,
|
||||
AssistantMessageContent,
|
||||
ThinkMessage,
|
||||
ThinkMessageContent,
|
||||
} from './messages/ConversationMessages.js';
|
||||
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
|
||||
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
|
||||
import { GeminiThoughtMessage } from './messages/GeminiThoughtMessage.js';
|
||||
import { GeminiThoughtMessageContent } from './messages/GeminiThoughtMessageContent.js';
|
||||
import { CompressionMessage } from './messages/CompressionMessage.js';
|
||||
import { SummaryMessage } from './messages/SummaryMessage.js';
|
||||
import { WarningMessage } from './messages/WarningMessage.js';
|
||||
import { RetryCountdownMessage } from './messages/RetryCountdownMessage.js';
|
||||
import {
|
||||
InfoMessage,
|
||||
WarningMessage,
|
||||
ErrorMessage,
|
||||
RetryCountdownMessage,
|
||||
} from './messages/StatusMessages.js';
|
||||
import { Box } from 'ink';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
|
|
@ -61,6 +65,11 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
embeddedShellFocused,
|
||||
availableTerminalHeightGemini,
|
||||
}) => {
|
||||
const marginTop =
|
||||
item.type === 'gemini_content' || item.type === 'gemini_thought_content'
|
||||
? 0
|
||||
: 1;
|
||||
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
const contentWidth = terminalWidth - 4;
|
||||
const boxWidth = mainAreaWidth || contentWidth;
|
||||
|
|
@ -69,6 +78,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
<Box
|
||||
flexDirection="column"
|
||||
key={itemForDisplay.id}
|
||||
marginTop={marginTop}
|
||||
marginLeft={2}
|
||||
marginRight={2}
|
||||
>
|
||||
|
|
@ -80,7 +90,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
<UserShellMessage text={itemForDisplay.text} />
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini' && (
|
||||
<GeminiMessage
|
||||
<AssistantMessage
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
|
|
@ -90,7 +100,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_content' && (
|
||||
<GeminiMessageContent
|
||||
<AssistantMessageContent
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
|
|
@ -100,7 +110,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought' && (
|
||||
<GeminiThoughtMessage
|
||||
<ThinkMessage
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
|
|
@ -110,7 +120,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought_content' && (
|
||||
<GeminiThoughtMessageContent
|
||||
<ThinkMessageContent
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
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
|
||||
|
|
@ -109,7 +110,8 @@ 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
|
||||
|
|
|
|||
261
packages/cli/src/ui/components/messages/ConversationMessages.tsx
Normal file
261
packages/cli/src/ui/components/messages/ConversationMessages.tsx
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import stringWidth from 'string-width';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import {
|
||||
SCREEN_READER_MODEL_PREFIX,
|
||||
SCREEN_READER_USER_PREFIX,
|
||||
} from '../../textConstants.js';
|
||||
|
||||
interface UserMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface UserShellMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface AssistantMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
interface AssistantMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
interface ThinkMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
interface ThinkMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
interface PrefixedTextMessageProps {
|
||||
text: string;
|
||||
prefix: string;
|
||||
prefixColor: string;
|
||||
textColor: string;
|
||||
ariaLabel?: string;
|
||||
marginTop?: number;
|
||||
alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end';
|
||||
}
|
||||
|
||||
interface PrefixedMarkdownMessageProps {
|
||||
text: string;
|
||||
prefix: string;
|
||||
prefixColor: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
ariaLabel?: string;
|
||||
textColor?: string;
|
||||
}
|
||||
|
||||
interface ContinuationMarkdownMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
basePrefix: string;
|
||||
textColor?: string;
|
||||
}
|
||||
|
||||
function getPrefixWidth(prefix: string): number {
|
||||
// Reserve one extra column so text never touches the prefix glyph.
|
||||
return stringWidth(prefix) + 1;
|
||||
}
|
||||
|
||||
const PrefixedTextMessage: React.FC<PrefixedTextMessageProps> = ({
|
||||
text,
|
||||
prefix,
|
||||
prefixColor,
|
||||
textColor,
|
||||
ariaLabel,
|
||||
marginTop = 0,
|
||||
alignSelf,
|
||||
}) => {
|
||||
const prefixWidth = getPrefixWidth(prefix);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
paddingY={0}
|
||||
marginTop={marginTop}
|
||||
alignSelf={alignSelf}
|
||||
>
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={prefixColor} aria-label={ariaLabel}>
|
||||
{prefix}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={textColor}>
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const PrefixedMarkdownMessage: React.FC<PrefixedMarkdownMessageProps> = ({
|
||||
text,
|
||||
prefix,
|
||||
prefixColor,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
ariaLabel,
|
||||
textColor,
|
||||
}) => {
|
||||
const prefixWidth = getPrefixWidth(prefix);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={prefixColor} aria-label={ariaLabel}>
|
||||
{prefix}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={textColor}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ContinuationMarkdownMessage: React.FC<
|
||||
ContinuationMarkdownMessageProps
|
||||
> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
basePrefix,
|
||||
textColor,
|
||||
}) => {
|
||||
const prefixWidth = getPrefixWidth(basePrefix);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth}>
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={textColor}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserMessage: React.FC<UserMessageProps> = ({ text }) => (
|
||||
<PrefixedTextMessage
|
||||
text={text}
|
||||
prefix=">"
|
||||
prefixColor={theme.text.accent}
|
||||
textColor={theme.text.accent}
|
||||
ariaLabel={SCREEN_READER_USER_PREFIX}
|
||||
alignSelf="flex-start"
|
||||
/>
|
||||
);
|
||||
|
||||
export const UserShellMessage: React.FC<UserShellMessageProps> = ({ text }) => {
|
||||
const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
|
||||
|
||||
return (
|
||||
<PrefixedTextMessage
|
||||
text={commandToDisplay}
|
||||
prefix="$"
|
||||
prefixColor={theme.text.link}
|
||||
textColor={theme.text.primary}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => (
|
||||
<PrefixedMarkdownMessage
|
||||
text={text}
|
||||
prefix="✦"
|
||||
prefixColor={theme.text.accent}
|
||||
ariaLabel={SCREEN_READER_MODEL_PREFIX}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AssistantMessageContent: React.FC<
|
||||
AssistantMessageContentProps
|
||||
> = ({ text, isPending, availableTerminalHeight, contentWidth }) => (
|
||||
<ContinuationMarkdownMessage
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth}
|
||||
basePrefix="✦"
|
||||
/>
|
||||
);
|
||||
|
||||
export const ThinkMessage: React.FC<ThinkMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => (
|
||||
<PrefixedMarkdownMessage
|
||||
text={text}
|
||||
prefix="✦"
|
||||
prefixColor={theme.text.secondary}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ThinkMessageContent: React.FC<ThinkMessageContentProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => (
|
||||
<ContinuationMarkdownMessage
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth}
|
||||
basePrefix="✦"
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface ErrorMessageProps {
|
||||
text: string;
|
||||
/** Optional inline hint displayed after the error text in secondary/dimmed color */
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an error message with a "✕" prefix.
|
||||
* When a hint is provided (e.g., retry countdown), it is displayed inline
|
||||
* in parentheses with a dimmed secondary color, similar to the ESC hint
|
||||
* style used in LoadingIndicator.
|
||||
*/
|
||||
export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text, hint }) => {
|
||||
const prefix = '✕ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.error}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexWrap="wrap" flexDirection="row">
|
||||
<Text color={theme.status.error}>{text}</Text>
|
||||
{hint && <Text color={theme.text.secondary}> ({hint})</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
|
||||
|
||||
interface GeminiMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
export const GeminiMessage: React.FC<GeminiMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const prefix = '✦ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.accent} aria-label={SCREEN_READER_MODEL_PREFIX}>
|
||||
{prefix}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
|
||||
interface GeminiMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gemini message content is a semi-hacked component. The intention is to represent a partial
|
||||
* of GeminiMessage and is only used when a response gets too long. In that instance messages
|
||||
* are split into multiple GeminiMessageContent's to enable the root <Static> component in
|
||||
* App.tsx to be as performant as humanly possible.
|
||||
*/
|
||||
export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const originalPrefix = '✦ ';
|
||||
const prefixWidth = originalPrefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth}>
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface GeminiThoughtMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays model thinking/reasoning text with a softer, dimmed style
|
||||
* to visually distinguish it from regular content output.
|
||||
*/
|
||||
export const GeminiThoughtMessage: React.FC<GeminiThoughtMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const prefix = '✦ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.secondary}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface GeminiThoughtMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuation component for thought messages, similar to GeminiMessageContent.
|
||||
* Used when a thought response gets too long and needs to be split for performance.
|
||||
*/
|
||||
export const GeminiThoughtMessageContent: React.FC<
|
||||
GeminiThoughtMessageContentProps
|
||||
> = ({ text, isPending, availableTerminalHeight, contentWidth }) => {
|
||||
const originalPrefix = '✦ ';
|
||||
const prefixWidth = originalPrefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth}>
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
|
||||
interface InfoMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
|
||||
// Don't render anything if text is empty
|
||||
if (!text || text.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = 'ℹ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.warning}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={theme.status.warning}>
|
||||
<RenderInline text={text} textColor={theme.status.warning} />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface RetryCountdownMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a retry countdown message in a dimmed/secondary style
|
||||
* to visually distinguish it from error messages.
|
||||
*/
|
||||
export const RetryCountdownMessage: React.FC<RetryCountdownMessageProps> = ({
|
||||
text,
|
||||
}) => {
|
||||
if (!text || text.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = '↻ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.secondary}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={theme.text.secondary}>
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
105
packages/cli/src/ui/components/messages/StatusMessages.tsx
Normal file
105
packages/cli/src/ui/components/messages/StatusMessages.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import stringWidth from 'string-width';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
|
||||
interface StatusMessageProps {
|
||||
text: string;
|
||||
prefix: string;
|
||||
prefixColor: string;
|
||||
textColor: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StatusTextProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared renderer for status-like history messages (info/warning/error/retry).
|
||||
* Keeps prefix spacing and wrapping behavior consistent across variants.
|
||||
*/
|
||||
export const StatusMessage: React.FC<StatusMessageProps> = ({
|
||||
text,
|
||||
prefix,
|
||||
prefixColor,
|
||||
textColor,
|
||||
children,
|
||||
}) => {
|
||||
if (!text || text.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefixWidth = stringWidth(prefix) + 1;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text color={prefixColor}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={textColor}>
|
||||
<RenderInline text={text} />
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfoMessage: React.FC<StatusTextProps> = ({ text }) => (
|
||||
<StatusMessage
|
||||
text={text}
|
||||
prefix="●"
|
||||
prefixColor={theme.text.primary}
|
||||
textColor={theme.text.primary}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SuccessMessage: React.FC<StatusTextProps> = ({ text }) => (
|
||||
<StatusMessage
|
||||
text={text}
|
||||
prefix="✓"
|
||||
prefixColor={theme.status.success}
|
||||
textColor={theme.status.success}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WarningMessage: React.FC<StatusTextProps> = ({ text }) => (
|
||||
<StatusMessage
|
||||
text={text}
|
||||
prefix="⚠"
|
||||
prefixColor={theme.status.warning}
|
||||
textColor={theme.status.warning}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ErrorMessage: React.FC<StatusTextProps & { hint?: string }> = ({
|
||||
text,
|
||||
hint,
|
||||
}) => (
|
||||
<StatusMessage
|
||||
text={text}
|
||||
prefix="✕"
|
||||
prefixColor={theme.status.error}
|
||||
textColor={theme.status.error}
|
||||
>
|
||||
{hint && <Text color={theme.text.secondary}> ({hint})</Text>}
|
||||
</StatusMessage>
|
||||
);
|
||||
|
||||
export const RetryCountdownMessage: React.FC<StatusTextProps> = ({ text }) => (
|
||||
<StatusMessage
|
||||
text={text}
|
||||
prefix="↻"
|
||||
prefixColor={theme.text.secondary}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js';
|
||||
import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js';
|
||||
|
||||
interface UserMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
|
||||
const prefix = '> ';
|
||||
const prefixWidth = prefix.length;
|
||||
const isSlashCommand = checkIsSlashCommand(text);
|
||||
|
||||
const textColor = isSlashCommand ? theme.text.accent : theme.text.secondary;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" paddingY={0} marginY={1} alignSelf="flex-start">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.accent} aria-label={SCREEN_READER_USER_PREFIX}>
|
||||
{prefix}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={textColor}>
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface UserShellMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const UserShellMessage: React.FC<UserShellMessageProps> = ({ text }) => {
|
||||
// Remove leading '!' if present, as App.tsx adds it for the processor.
|
||||
const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.link}>$ </Text>
|
||||
<Text color={theme.text.primary}>{commandToDisplay}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface WarningMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const WarningMessage: React.FC<WarningMessageProps> = ({ text }) => {
|
||||
const prefix = '⚠ ';
|
||||
const prefixWidth = 3;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={Colors.AccentYellow}>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" color={Colors.AccentYellow}>
|
||||
<RenderInline text={text} textColor={theme.status.warning} />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue