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:
chiga0 2026-04-04 20:43:06 +08:00
parent b9c17d13ff
commit 6fd29b698b
18 changed files with 261 additions and 27 deletions

View file

@ -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>
);
};

View file

@ -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 &&

View file

@ -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">