mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +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
322
packages/cli/src/ui/commands/hooksCommand.ts
Normal file
322
packages/cli/src/ui/commands/hooksCommand.ts
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
SlashCommand,
|
||||
SlashCommandActionReturn,
|
||||
CommandContext,
|
||||
MessageActionReturn,
|
||||
} from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import type { HookRegistryEntry } from '@qwen-code/qwen-code-core';
|
||||
|
||||
/**
|
||||
* Format hook source for display
|
||||
*/
|
||||
function formatHookSource(source: string): string {
|
||||
switch (source) {
|
||||
case 'project':
|
||||
return 'Project';
|
||||
case 'user':
|
||||
return 'User';
|
||||
case 'system':
|
||||
return 'System';
|
||||
case 'extensions':
|
||||
return 'Extension';
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format hook status for display
|
||||
*/
|
||||
function formatHookStatus(enabled: boolean): string {
|
||||
return enabled ? '✓ Enabled' : '✗ Disabled';
|
||||
}
|
||||
|
||||
const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
get description() {
|
||||
return t('List all configured hooks');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
_args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Config not loaded.'),
|
||||
};
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t(
|
||||
'Hooks are not enabled. Enable hooks in settings to use this feature.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
const allHooks = registry.getAllHooks();
|
||||
|
||||
if (allHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t(
|
||||
'No hooks configured. Add hooks in your settings.json file.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Group hooks by event
|
||||
const hooksByEvent = new Map<string, HookRegistryEntry[]>();
|
||||
for (const hook of allHooks) {
|
||||
const eventName = hook.eventName;
|
||||
if (!hooksByEvent.has(eventName)) {
|
||||
hooksByEvent.set(eventName, []);
|
||||
}
|
||||
hooksByEvent.get(eventName)!.push(hook);
|
||||
}
|
||||
|
||||
let output = `**Configured Hooks (${allHooks.length} total)**\n\n`;
|
||||
|
||||
for (const [eventName, hooks] of hooksByEvent) {
|
||||
output += `### ${eventName}\n`;
|
||||
for (const hook of hooks) {
|
||||
const name = hook.config.name || hook.config.command || 'unnamed';
|
||||
const source = formatHookSource(hook.source);
|
||||
const status = formatHookStatus(hook.enabled);
|
||||
const matcher = hook.matcher ? ` (matcher: ${hook.matcher})` : '';
|
||||
output += `- **${name}** [${source}] ${status}${matcher}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: output,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const enableCommand: SlashCommand = {
|
||||
name: 'enable',
|
||||
get description() {
|
||||
return t('Enable a disabled hook');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
const hookName = args.trim();
|
||||
if (!hookName) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t(
|
||||
'Please specify a hook name. Usage: /hooks enable <hook-name>',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Config not loaded.'),
|
||||
};
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Hooks are not enabled.'),
|
||||
};
|
||||
}
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
registry.setHookEnabled(hookName, true);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t('Hook "{{name}}" has been enabled for this session.', {
|
||||
name: hookName,
|
||||
}),
|
||||
};
|
||||
},
|
||||
completion: async (context: CommandContext, partialArg: string) => {
|
||||
const { config } = context.services;
|
||||
if (!config) return [];
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
const allHooks = registry.getAllHooks();
|
||||
|
||||
// Return disabled hooks for enable command (deduplicated by name)
|
||||
const disabledHookNames = allHooks
|
||||
.filter((hook) => !hook.enabled)
|
||||
.map((hook) => hook.config.name || hook.config.command || '')
|
||||
.filter((name) => name && name.startsWith(partialArg));
|
||||
return [...new Set(disabledHookNames)];
|
||||
},
|
||||
};
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
name: 'disable',
|
||||
get description() {
|
||||
return t('Disable an active hook');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
const hookName = args.trim();
|
||||
if (!hookName) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t(
|
||||
'Please specify a hook name. Usage: /hooks disable <hook-name>',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Config not loaded.'),
|
||||
};
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Hooks are not enabled.'),
|
||||
};
|
||||
}
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
registry.setHookEnabled(hookName, false);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: t('Hook "{{name}}" has been disabled for this session.', {
|
||||
name: hookName,
|
||||
}),
|
||||
};
|
||||
},
|
||||
completion: async (context: CommandContext, partialArg: string) => {
|
||||
const { config } = context.services;
|
||||
if (!config) return [];
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const registry = hookSystem.getRegistry();
|
||||
const allHooks = registry.getAllHooks();
|
||||
|
||||
// Return enabled hooks for disable command (deduplicated by name)
|
||||
const enabledHookNames = allHooks
|
||||
.filter((hook) => hook.enabled)
|
||||
.map((hook) => hook.config.name || hook.config.command || '')
|
||||
.filter((name) => name && name.startsWith(partialArg));
|
||||
return [...new Set(enabledHookNames)];
|
||||
},
|
||||
};
|
||||
|
||||
export const hooksCommand: SlashCommand = {
|
||||
name: 'hooks',
|
||||
get description() {
|
||||
return t('Manage Qwen Code hooks');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [listCommand, enableCommand, disableCommand],
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<SlashCommandActionReturn> => {
|
||||
// If no subcommand provided, show list
|
||||
if (!args.trim()) {
|
||||
const result = await listCommand.action?.(context, '');
|
||||
return result ?? { type: 'message', messageType: 'info', content: '' };
|
||||
}
|
||||
|
||||
const [subcommand, ...rest] = args.trim().split(/\s+/);
|
||||
const subArgs = rest.join(' ');
|
||||
|
||||
let result: SlashCommandActionReturn | void;
|
||||
switch (subcommand.toLowerCase()) {
|
||||
case 'list':
|
||||
result = await listCommand.action?.(context, subArgs);
|
||||
break;
|
||||
case 'enable':
|
||||
result = await enableCommand.action?.(context, subArgs);
|
||||
break;
|
||||
case 'disable':
|
||||
result = await disableCommand.action?.(context, subArgs);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t(
|
||||
'Unknown subcommand: {{cmd}}. Available: list, enable, disable',
|
||||
{
|
||||
cmd: subcommand,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
return result ?? { type: 'message', messageType: 'info', content: '' };
|
||||
},
|
||||
completion: async (context: CommandContext, partialArg: string) => {
|
||||
const subcommands = ['list', 'enable', 'disable'];
|
||||
const parts = partialArg.split(/\s+/);
|
||||
|
||||
if (parts.length <= 1) {
|
||||
// Complete subcommand
|
||||
return subcommands.filter((cmd) => cmd.startsWith(partialArg));
|
||||
}
|
||||
|
||||
// Complete subcommand arguments
|
||||
const [subcommand, ...rest] = parts;
|
||||
const subArgs = rest.join(' ');
|
||||
|
||||
switch (subcommand.toLowerCase()) {
|
||||
case 'enable':
|
||||
return enableCommand.completion?.(context, subArgs) ?? [];
|
||||
case 'disable':
|
||||
return disableCommand.completion?.(context, subArgs) ?? [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1335,6 +1335,40 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('Printable CSI-u keys', () => {
|
||||
it('parses kitty CSI-u space as a space key with literal sequence', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[32u`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'space',
|
||||
sequence: ' ',
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('parses kitty CSI-u printable letters as literal input', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[100u`)); // 'd'
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'd',
|
||||
sequence: 'd',
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shift+Tab forms', () => {
|
||||
it.each([
|
||||
{ sequence: `\x1b[Z`, description: 'legacy reverse Tab' },
|
||||
|
|
|
|||
|
|
@ -332,6 +332,36 @@ export function KeypressProvider({
|
|||
};
|
||||
}
|
||||
|
||||
// Printable CSI-u keys (including space) should behave like regular
|
||||
// character input so downstream text inputs receive the literal char.
|
||||
if (
|
||||
terminator === 'u' &&
|
||||
!ctrl &&
|
||||
keyCode >= 32 &&
|
||||
keyCode !== 127 &&
|
||||
keyCode <= 0x10ffff
|
||||
) {
|
||||
const char = String.fromCodePoint(keyCode);
|
||||
const printableName =
|
||||
char === ' '
|
||||
? 'space'
|
||||
: /^[A-Za-z]$/.test(char)
|
||||
? char.toLowerCase()
|
||||
: char;
|
||||
return {
|
||||
key: {
|
||||
name: printableName,
|
||||
ctrl: false,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: char,
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
// Ctrl+letters
|
||||
if (
|
||||
ctrl &&
|
||||
|
|
|
|||
|
|
@ -1038,6 +1038,15 @@ export const useGeminiStream = (
|
|||
clearRetryCountdown();
|
||||
}
|
||||
break;
|
||||
case ServerGeminiEventType.HookSystemMessage:
|
||||
// Display system message from hooks (e.g., Ralph Loop iteration info)
|
||||
// This is handled as a content event to show in the UI
|
||||
geminiMessageBuffer = handleContentEvent(
|
||||
event.value + '\n',
|
||||
geminiMessageBuffer,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
break;
|
||||
default: {
|
||||
// enforces exhaustive switch-case
|
||||
const unreachable: never = event;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const noColorSemanticColors: SemanticColors = {
|
|||
secondary: '',
|
||||
link: '',
|
||||
accent: '',
|
||||
code: '',
|
||||
},
|
||||
background: {
|
||||
primary: '',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface SemanticColors {
|
|||
secondary: string;
|
||||
link: string;
|
||||
accent: string;
|
||||
code: string;
|
||||
};
|
||||
background: {
|
||||
primary: string;
|
||||
|
|
@ -45,6 +46,7 @@ export const lightSemanticColors: SemanticColors = {
|
|||
secondary: lightTheme.Gray,
|
||||
link: lightTheme.AccentBlue,
|
||||
accent: lightTheme.AccentPurple,
|
||||
code: lightTheme.LightBlue,
|
||||
},
|
||||
background: {
|
||||
primary: lightTheme.Background,
|
||||
|
|
@ -77,6 +79,7 @@ export const darkSemanticColors: SemanticColors = {
|
|||
secondary: darkTheme.Gray,
|
||||
link: darkTheme.AccentBlue,
|
||||
accent: darkTheme.AccentPurple,
|
||||
code: darkTheme.LightBlue,
|
||||
},
|
||||
background: {
|
||||
primary: darkTheme.Background,
|
||||
|
|
@ -109,6 +112,7 @@ export const ansiSemanticColors: SemanticColors = {
|
|||
secondary: ansiTheme.Gray,
|
||||
link: ansiTheme.AccentBlue,
|
||||
accent: ansiTheme.AccentPurple,
|
||||
code: ansiTheme.LightBlue,
|
||||
},
|
||||
background: {
|
||||
primary: ansiTheme.Background,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface CustomTheme {
|
|||
secondary?: string;
|
||||
link?: string;
|
||||
accent?: string;
|
||||
code?: string;
|
||||
};
|
||||
background?: {
|
||||
primary?: string;
|
||||
|
|
@ -174,6 +175,7 @@ export class Theme {
|
|||
secondary: this.colors.Gray,
|
||||
link: this.colors.AccentBlue,
|
||||
accent: this.colors.AccentPurple,
|
||||
code: this.colors.LightBlue,
|
||||
},
|
||||
background: {
|
||||
primary: this.colors.Background,
|
||||
|
|
@ -269,7 +271,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
|||
type: 'custom',
|
||||
Background: customTheme.background?.primary ?? customTheme.Background ?? '',
|
||||
Foreground: customTheme.text?.primary ?? customTheme.Foreground ?? '',
|
||||
LightBlue: customTheme.text?.link ?? customTheme.LightBlue ?? '',
|
||||
LightBlue: customTheme.text?.code ?? customTheme.LightBlue ?? '',
|
||||
AccentBlue: customTheme.text?.link ?? customTheme.AccentBlue ?? '',
|
||||
AccentPurple: customTheme.text?.accent ?? customTheme.AccentPurple ?? '',
|
||||
AccentCyan: customTheme.text?.link ?? customTheme.AccentCyan ?? '',
|
||||
|
|
@ -433,6 +435,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
|||
secondary: customTheme.text?.secondary ?? colors.Gray,
|
||||
link: customTheme.text?.link ?? colors.AccentBlue,
|
||||
accent: customTheme.text?.accent ?? colors.AccentPurple,
|
||||
code: customTheme.text?.code ?? colors.LightBlue,
|
||||
},
|
||||
background: {
|
||||
primary: customTheme.background?.primary ?? colors.Background,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue