feat: to #2767, support verbose and compact mode swither with ctrl-o

This commit is contained in:
秦奇 2026-03-31 19:00:13 +08:00
parent 1b1a029fd7
commit b9c17d13ff
17 changed files with 166 additions and 45 deletions

View file

@ -16,6 +16,7 @@ import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
@ -23,6 +24,7 @@ export const Footer: React.FC = () => {
const uiState = useUIState();
const config = useConfig();
const { vimEnabled, vimMode } = useVimMode();
const { verboseMode } = useVerboseMode();
const { promptTokenCount, showAutoAcceptIndicator } = {
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
@ -93,6 +95,12 @@ export const Footer: React.FC = () => {
),
});
}
if (verboseMode) {
rightItems.push({
key: 'verbose',
node: <Text color={theme.text.accent}>{t('verbose')}</Text>,
});
}
return (
<Box
justifyContent="space-between"

View file

@ -43,6 +43,7 @@ import { ContextUsage } from './views/ContextUsage.js';
import { ArenaAgentCard, ArenaSessionCard } from './arena/ArenaCards.js';
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
import { BtwMessage } from './messages/BtwMessage.js';
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
interface HistoryItemDisplayProps {
item: HistoryItem;
@ -74,6 +75,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
? 0
: 1;
const { verboseMode } = useVerboseMode();
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
const contentWidth = terminalWidth - 4;
const boxWidth = mainAreaWidth || contentWidth;
@ -113,7 +115,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
contentWidth={contentWidth}
/>
)}
{itemForDisplay.type === 'gemini_thought' && (
{verboseMode && itemForDisplay.type === 'gemini_thought' && (
<ThinkMessage
text={itemForDisplay.text}
isPending={isPending}
@ -123,7 +125,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
contentWidth={contentWidth}
/>
)}
{itemForDisplay.type === 'gemini_thought_content' && (
{verboseMode && itemForDisplay.type === 'gemini_thought_content' && (
<ThinkMessageContent
text={itemForDisplay.text}
isPending={isPending}

View file

@ -13,6 +13,7 @@ import { useUIState } from '../contexts/UIStateContext.js';
import { useAppContext } from '../contexts/AppContext.js';
import { AppHeader } from './AppHeader.js';
import { DebugModeNotification } from './DebugModeNotification.js';
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
// Limit Gemini messages to a very high number of lines to mitigate performance
// issues in the worst case if we somehow get an enormous response from Gemini.
@ -23,6 +24,7 @@ const MAX_GEMINI_MESSAGE_LINES = 65536;
export const MainContent = () => {
const { version } = useAppContext();
const uiState = useUIState();
const { frozenSnapshot } = useVerboseMode();
const {
pendingHistoryItems,
terminalWidth,
@ -57,21 +59,26 @@ export const MainContent = () => {
</Static>
<OverflowProvider>
<Box flexDirection="column">
{pendingHistoryItems.map((item, i) => (
<HistoryItemDisplay
key={i}
availableTerminalHeight={
uiState.constrainHeight ? availableTerminalHeight : undefined
}
terminalWidth={terminalWidth}
mainAreaWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
))}
{(frozenSnapshot ?? pendingHistoryItems).map((item, i) => {
const isFrozen = frozenSnapshot !== null;
return (
<HistoryItemDisplay
key={i}
availableTerminalHeight={
uiState.constrainHeight ? availableTerminalHeight : undefined
}
terminalWidth={terminalWidth}
mainAreaWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={isFrozen ? false : !uiState.isEditorDialogOpen}
activeShellPtyId={isFrozen ? undefined : uiState.activePtyId}
embeddedShellFocused={
isFrozen ? false : uiState.embeddedShellFocused
}
/>
);
})}
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
</Box>
</OverflowProvider>

View file

@ -12,6 +12,7 @@ import { StreamingState, ToolCallStatus } from '../../types.js';
import { Text } from 'ink';
import { StreamingContext } from '../../contexts/StreamingContext.js';
import { SettingsContext } from '../../contexts/SettingsContext.js';
import { VerboseModeProvider } from '../../contexts/VerboseModeContext.js';
import type {
AnsiOutput,
AnsiOutputDisplay,
@ -101,18 +102,21 @@ const mockSettings: LoadedSettings = {
},
} as LoadedSettings;
// Helper to render with context
// Helper to render with context (verbose=true by default to show tool output)
const renderWithContext = (
ui: React.ReactElement,
streamingState: StreamingState,
verboseMode = true,
) => {
const contextValue: StreamingState = streamingState;
return render(
<SettingsContext.Provider value={mockSettings}>
<StreamingContext.Provider value={contextValue}>
{ui}
</StreamingContext.Provider>
</SettingsContext.Provider>,
<VerboseModeProvider value={{ verboseMode, frozenSnapshot: null }}>
<SettingsContext.Provider value={mockSettings}>
<StreamingContext.Provider value={contextValue}>
{ui}
</StreamingContext.Provider>
</SettingsContext.Provider>
</VerboseModeProvider>,
);
};
@ -143,6 +147,18 @@ describe('<ToolMessage />', () => {
expect(output).toContain('MockMarkdown:Test result');
});
it('hides result output in compact mode (verboseMode=false)', () => {
const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} />,
StreamingState.Idle,
false, // compact mode
);
const output = lastFrame();
expect(output).toContain('✓'); // status indicator still visible
expect(output).toContain('test-tool'); // tool name still visible
expect(output).not.toContain('MockMarkdown:Test result'); // result hidden
});
describe('ToolStatusIndicator rendering', () => {
it('shows ✓ for Success status', () => {
const { lastFrame } = renderWithContext(

View file

@ -33,6 +33,7 @@ import {
import { theme } from '../../semantic-colors.js';
import { useSettings } from '../../contexts/SettingsContext.js';
import type { LoadedSettings } from '../../../config/settings.js';
import { useVerboseMode } from '../../contexts/VerboseModeContext.js';
const STATIC_HEIGHT = 1;
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
@ -324,6 +325,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 };
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
@ -344,44 +349,44 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
)}
{emphasis === 'high' && <TrailingIndicator />}
</Box>
{displayRenderer.type !== 'none' && (
{effectiveDisplayRenderer.type !== 'none' && (
<Box paddingLeft={STATUS_INDICATOR_WIDTH} width="100%" marginTop={1}>
<Box flexDirection="column">
{displayRenderer.type === 'todo' && (
<TodoResultRenderer data={displayRenderer.data} />
{effectiveDisplayRenderer.type === 'todo' && (
<TodoResultRenderer data={effectiveDisplayRenderer.data} />
)}
{displayRenderer.type === 'plan' && (
{effectiveDisplayRenderer.type === 'plan' && (
<PlanResultRenderer
data={displayRenderer.data}
data={effectiveDisplayRenderer.data}
availableHeight={availableHeight}
childWidth={innerWidth}
/>
)}
{displayRenderer.type === 'task' && config && (
{effectiveDisplayRenderer.type === 'task' && config && (
<SubagentExecutionRenderer
data={displayRenderer.data}
data={effectiveDisplayRenderer.data}
availableHeight={availableHeight}
childWidth={innerWidth}
config={config}
/>
)}
{displayRenderer.type === 'diff' && (
{effectiveDisplayRenderer.type === 'diff' && (
<DiffResultRenderer
data={displayRenderer.data}
data={effectiveDisplayRenderer.data}
availableHeight={availableHeight}
childWidth={innerWidth}
settings={settings}
/>
)}
{displayRenderer.type === 'ansi' && (
{effectiveDisplayRenderer.type === 'ansi' && (
<AnsiOutputText
data={displayRenderer.data}
data={effectiveDisplayRenderer.data}
availableTerminalHeight={availableHeight}
/>
)}
{displayRenderer.type === 'string' && (
{effectiveDisplayRenderer.type === 'string' && (
<StringResultRenderer
data={displayRenderer.data}
data={effectiveDisplayRenderer.data}
renderAsMarkdown={renderOutputAsMarkdown}
availableHeight={availableHeight}
childWidth={innerWidth}