mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 19:52:02 +00:00
fix: address PR review feedback for verbose/compact mode toggle
- Change default verboseMode to true (preserving current UX behavior) - Fix compact mode hiding active shell output (add forceShowResult + isUserInitiated) - Fix asymmetric frozen snapshot (freeze on ANY toggle during streaming) - Fix copyright header in VerboseModeContext.tsx (Google LLC → Qwen) - Add proper translations for all 6 locales (de/ja/pt/ru/zh/en) - Rewrite CompactToolGroupDisplay with bordered box, i18n hint, shell detection - Fix Pending status color (theme.text.secondary instead of theme.status.success) - Fix description casing: ctrl+o → Ctrl+O - Add explanatory comment for useCallback settings dependency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9c17d13ff
commit
6fd29b698b
18 changed files with 261 additions and 27 deletions
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||
import {
|
||||
TOOL_STATUS,
|
||||
SHELL_COMMAND_NAME,
|
||||
SHELL_NAME,
|
||||
} from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface CompactToolGroupDisplayProps {
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
// Priority: Confirming > Executing > Error > Canceled > Pending > Success
|
||||
function getOverallStatus(
|
||||
toolCalls: IndividualToolCallDisplay[],
|
||||
): ToolCallStatus {
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Confirming))
|
||||
return ToolCallStatus.Confirming;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Executing))
|
||||
return ToolCallStatus.Executing;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Error))
|
||||
return ToolCallStatus.Error;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Canceled))
|
||||
return ToolCallStatus.Canceled;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Pending))
|
||||
return ToolCallStatus.Pending;
|
||||
return ToolCallStatus.Success;
|
||||
}
|
||||
|
||||
// Active tool priority: Confirming > Executing > last in array
|
||||
function getActiveTool(
|
||||
toolCalls: IndividualToolCallDisplay[],
|
||||
): IndividualToolCallDisplay {
|
||||
return (
|
||||
toolCalls.find((t) => t.status === ToolCallStatus.Confirming) ??
|
||||
toolCalls.find((t) => t.status === ToolCallStatus.Executing) ??
|
||||
toolCalls[toolCalls.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
const STATUS_INDICATOR_WIDTH = 3;
|
||||
|
||||
export const CompactToolGroupDisplay: React.FC<
|
||||
CompactToolGroupDisplayProps
|
||||
> = ({ toolCalls, contentWidth }) => {
|
||||
if (toolCalls.length === 0) return null;
|
||||
|
||||
const overallStatus = getOverallStatus(toolCalls);
|
||||
const activeTool = getActiveTool(toolCalls);
|
||||
|
||||
const isShellCommand = toolCalls.some(
|
||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
||||
);
|
||||
const hasPending = !toolCalls.every(
|
||||
(t) => t.status === ToolCallStatus.Success,
|
||||
);
|
||||
|
||||
const borderColor = isShellCommand
|
||||
? theme.ui.symbol
|
||||
: hasPending
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
// Take only the first line of description to prevent multi-line shell scripts
|
||||
// from expanding the compact view (wrap="truncate-end" only handles width overflow,
|
||||
// not literal \n characters in the content)
|
||||
const activeToolDescription = activeTool.description
|
||||
? activeTool.description.split('\n')[0]
|
||||
: '';
|
||||
|
||||
const renderStatusIcon = () => {
|
||||
switch (overallStatus) {
|
||||
case ToolCallStatus.Executing:
|
||||
return (
|
||||
<GeminiRespondingSpinner
|
||||
spinnerType="toggle"
|
||||
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
|
||||
/>
|
||||
);
|
||||
case ToolCallStatus.Success:
|
||||
return <Text color={theme.status.success}>{TOOL_STATUS.SUCCESS}</Text>;
|
||||
case ToolCallStatus.Error:
|
||||
return (
|
||||
<Text color={theme.status.error} bold>
|
||||
{TOOL_STATUS.ERROR}
|
||||
</Text>
|
||||
);
|
||||
case ToolCallStatus.Confirming:
|
||||
return (
|
||||
<Text color={theme.status.warning}>{TOOL_STATUS.CONFIRMING}</Text>
|
||||
);
|
||||
case ToolCallStatus.Canceled:
|
||||
return (
|
||||
<Text color={theme.status.warning} bold>
|
||||
{TOOL_STATUS.CANCELED}
|
||||
</Text>
|
||||
);
|
||||
case ToolCallStatus.Pending:
|
||||
return <Text color={theme.text.secondary}>{TOOL_STATUS.PENDING}</Text>;
|
||||
default:
|
||||
return <Text>{TOOL_STATUS.PENDING}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
width={contentWidth}
|
||||
borderDimColor={hasPending}
|
||||
borderColor={borderColor}
|
||||
gap={0}
|
||||
>
|
||||
{/* Status line: icon + tool name + description */}
|
||||
<Box flexDirection="row">
|
||||
<Box minWidth={STATUS_INDICATOR_WIDTH}>{renderStatusIcon()}</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="truncate-end">
|
||||
<Text bold>{activeTool.name}</Text>
|
||||
{activeToolDescription ? (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
{activeToolDescription}
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Hint line */}
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Press Ctrl+O to show full tool output')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -11,9 +11,11 @@ import type { IndividualToolCallDisplay } from '../../types.js';
|
|||
import { ToolCallStatus } from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import { CompactToolGroupDisplay } from './CompactToolGroupDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
import { useVerboseMode } from '../../contexts/VerboseModeContext.js';
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
|
|
@ -24,6 +26,7 @@ interface ToolGroupMessageProps {
|
|||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
onShellInputSubmit?: (input: string) => void;
|
||||
isUserInitiated?: boolean;
|
||||
}
|
||||
|
||||
// Main component renders the border and maps the tools using ToolMessage
|
||||
|
|
@ -34,7 +37,14 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
isFocused = true,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
isUserInitiated,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const { verboseMode } = useVerboseMode();
|
||||
|
||||
const hasConfirmingTool = toolCalls.some(
|
||||
(t) => t.status === ToolCallStatus.Confirming,
|
||||
);
|
||||
const isEmbeddedShellFocused =
|
||||
embeddedShellFocused &&
|
||||
toolCalls.some(
|
||||
|
|
@ -42,11 +52,34 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
t.ptyId === activeShellPtyId && t.status === ToolCallStatus.Executing,
|
||||
);
|
||||
|
||||
// useMemo must be called unconditionally (Rules of Hooks) — before any early return
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
);
|
||||
|
||||
// Compact mode: entire group → single line summary
|
||||
// Force-expand when: user must interact (Confirming), shell is focused, or user-initiated
|
||||
const showCompact =
|
||||
!verboseMode &&
|
||||
!hasConfirmingTool &&
|
||||
!isEmbeddedShellFocused &&
|
||||
!isUserInitiated;
|
||||
|
||||
if (showCompact) {
|
||||
return (
|
||||
<CompactToolGroupDisplay
|
||||
toolCalls={toolCalls}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Full expanded view
|
||||
const hasPending = !toolCalls.every(
|
||||
(t) => t.status === ToolCallStatus.Success,
|
||||
);
|
||||
|
||||
const config = useConfig();
|
||||
const isShellCommand = toolCalls.some(
|
||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
||||
);
|
||||
|
|
@ -61,13 +94,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
// account for border (2 chars) and padding (2 chars)
|
||||
const innerWidth = contentWidth - 4;
|
||||
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
);
|
||||
|
||||
let countToolCallsWithResults = 0;
|
||||
for (const tool of toolCalls) {
|
||||
if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
|
||||
|
|
@ -121,6 +147,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
config={config}
|
||||
forceShowResult={isUserInitiated}
|
||||
/>
|
||||
</Box>
|
||||
{tool.status === ToolCallStatus.Confirming &&
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
|||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
config?: Config;
|
||||
forceShowResult?: boolean;
|
||||
}
|
||||
|
||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
|
|
@ -264,6 +265,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
embeddedShellFocused,
|
||||
ptyId,
|
||||
config,
|
||||
forceShowResult,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const isThisShellFocused =
|
||||
|
|
@ -326,9 +328,10 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
// Use the custom hook to determine the display type
|
||||
const displayRenderer = useResultDisplayRenderer(resultDisplay);
|
||||
const { verboseMode } = useVerboseMode();
|
||||
const effectiveDisplayRenderer = verboseMode
|
||||
? displayRenderer
|
||||
: { type: 'none' as const };
|
||||
const effectiveDisplayRenderer =
|
||||
verboseMode || forceShowResult
|
||||
? displayRenderer
|
||||
: { type: 'none' as const };
|
||||
|
||||
return (
|
||||
<Box paddingX={1} paddingY={0} flexDirection="column">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue