Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum Command {
EXIT = 'exit',
SHOW_MORE_LINES = 'showMoreLines',
RETRY_LAST = 'retryLast',
TOGGLE_VERBOSE_MODE = 'toggleVerboseMode',

// Shell commands
REVERSE_SEARCH = 'reverseSearch',
Expand Down Expand Up @@ -172,6 +173,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.EXIT]: [{ key: 'd', ctrl: true }],
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
[Command.RETRY_LAST]: [{ key: 'y', ctrl: true }],
[Command.TOGGLE_VERBOSE_MODE]: [{ key: 'o', ctrl: true }],

// Shell commands
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,16 @@ const SETTINGS_SCHEMA = {
description: 'The last time the feedback dialog was shown.',
showInDialog: false,
},
verboseMode: {
type: 'boolean',
label: 'Verbose Mode',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).',
showInDialog: false,
},
},
},

Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -1948,4 +1948,9 @@ export default {
'Raw-Modus nicht verfügbar. Bitte in einem interaktiven Terminal ausführen.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ Pfeiltasten zum Navigieren, Enter zum Auswählen, Strg+C zum Beenden)\n',
verbose: 'ausführlich',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
'Press Ctrl+O to show full tool output':
'Strg+O für vollständige Tool-Ausgabe drücken',
};
5 changes: 5 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1989,4 +1989,9 @@ export default {
'Raw mode not available. Please run in an interactive terminal.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n',
verbose: 'verbose',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Press Ctrl+O to show full tool output',
};
4 changes: 4 additions & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -1441,4 +1441,8 @@ export default {
'Rawモードが利用できません。インタラクティブターミナルで実行してください。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ 矢印キーで移動、Enter で選択、Ctrl+C で終了)\n',
verbose: '詳細',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。',
'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示',
};
5 changes: 5 additions & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -1938,4 +1938,9 @@ export default {
'Modo raw não disponível. Execute em um terminal interativo.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(Use ↑ ↓ para navegar, Enter para selecionar, Ctrl+C para sair)\n',
verbose: 'detalhado',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Mostrar saída completa da ferramenta e raciocínio no modo detalhado (alternar com Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Pressione Ctrl+O para exibir a saída completa da ferramenta',
};
5 changes: 5 additions & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -1945,4 +1945,9 @@ export default {
'Raw-режим недоступен. Пожалуйста, запустите в интерактивном терминале.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ стрелки для навигации, Enter для выбора, Ctrl+C для выхода)\n',
verbose: 'подробный',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Нажмите Ctrl+O для показа полного вывода инструментов',
};
4 changes: 4 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -1795,4 +1795,8 @@ export default {
'原始模式不可用。请在交互式终端中运行。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(使用 ↑ ↓ 箭头导航,Enter 选择,Ctrl+C 退出)\n',
verbose: '详细',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。',
'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果',
};
65 changes: 56 additions & 9 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { useApprovalModeCommand } from './hooks/useApprovalModeCommand.js';
import { useResumeCommand } from './hooks/useResumeCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { VerboseModeProvider } from './contexts/VerboseModeContext.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { useStdin, useStdout } from 'ink';
Expand Down Expand Up @@ -792,6 +793,11 @@ export const AppContainer = (props: AppContainerProps) => {
handleWelcomeBackClose,
} = useWelcomeBack(config, handleFinalSubmit, buffer, settings.merged);

const pendingHistoryItems = useMemo(
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);

cancelHandlerRef.current = useCallback(() => {
const pendingHistoryItems = [
...pendingSlashCommandHistoryItems,
Expand Down Expand Up @@ -963,6 +969,13 @@ export const AppContainer = (props: AppContainerProps) => {
const [showToolDescriptions, setShowToolDescriptions] =
useState<boolean>(false);

const [verboseMode, setVerboseMode] = useState<boolean>(
settings.merged.ui?.verboseMode ?? true,
);
const [frozenSnapshot, setFrozenSnapshot] = useState<
HistoryItemWithoutId[] | null
>(null);

const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null);
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
Expand All @@ -977,6 +990,18 @@ export const AppContainer = (props: AppContainerProps) => {
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false);

useEffect(() => {
// Clear frozen snapshot when streaming ends OR when entering confirmation
// state. During WaitingForConfirmation, the user needs to see the latest
// pending items (including the confirmation message) rather than a stale snapshot.
if (
streamingState === StreamingState.Idle ||
streamingState === StreamingState.WaitingForConfirmation
) {
setFrozenSnapshot(null);
}
}, [streamingState]);

const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder);
const {
Expand Down Expand Up @@ -1347,6 +1372,18 @@ export const AppContainer = (props: AppContainerProps) => {
if (activePtyId || embeddedShellFocused) {
setEmbeddedShellFocused((prev) => !prev);
}
} else if (keyMatchers[Command.TOGGLE_VERBOSE_MODE](key)) {
const newValue = !verboseMode;
setVerboseMode(newValue);
void settings.setValue(SettingScope.User, 'ui.verboseMode', newValue);
refreshStatic();
// Only freeze during the actual responding phase. WaitingForConfirmation
// must keep focus so the user can approve/cancel tool confirmation UI.
if (streamingState === StreamingState.Responding) {
setFrozenSnapshot([...pendingHistoryItems]);
} else {
setFrozenSnapshot(null);
}
}
},
[
Expand Down Expand Up @@ -1375,8 +1412,16 @@ export const AppContainer = (props: AppContainerProps) => {
btwItem,
setBtwItem,
cancelBtw,
settings.merged.general?.debugKeystrokeLogging,
// `settings` is a stable LoadedSettings instance (not recreated on render).
// ESLint requires it here because the callback calls settings.setValue().
// debugKeystrokeLogging is read at call time, so no stale closure risk.
settings,
isAuthenticating,
verboseMode,
setVerboseMode,
setFrozenSnapshot,
pendingHistoryItems,
refreshStatic,
],
);

Expand Down Expand Up @@ -1465,11 +1510,6 @@ export const AppContainer = (props: AppContainerProps) => {
sessionStats,
});

const pendingHistoryItems = useMemo(
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);

const uiState: UIState = useMemo(
() => ({
history: historyManager.history,
Expand Down Expand Up @@ -1797,6 +1837,11 @@ export const AppContainer = (props: AppContainerProps) => {
],
);

const verboseModeValue = useMemo(
() => ({ verboseMode, frozenSnapshot }),
[verboseMode, frozenSnapshot],
);

return (
<UIStateContext.Provider value={uiState}>
<UIActionsContext.Provider value={uiActions}>
Expand All @@ -1807,9 +1852,11 @@ export const AppContainer = (props: AppContainerProps) => {
startupWarnings: props.startupWarnings || [],
}}
>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
<VerboseModeProvider value={verboseModeValue}>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
</VerboseModeProvider>
</AppContext.Provider>
</ConfigContext.Provider>
</UIActionsContext.Provider>
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/ui/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
Comment on lines 17 to 18
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Footer.test.tsx doesn't wrap <Footer /> in VerboseModeProvider. Footer now calls useVerboseMode() to conditionally render the "verbose" label, but tests rely on the default context value (verboseMode: true). No test verifies the label disappears when in compact mode.

Suggested fix:

import { VerboseModeProvider } from '../contexts/VerboseModeContext.js';
// In renderWithWidth, wrap Footer:
<VerboseModeProvider value={{ verboseMode: true, frozenSnapshot: null }}>
  {/* ... existing providers ... */}
  <Footer />
</VerboseModeProvider>

// Add test:
it('does not show verbose label when verboseMode is false', () => {
  // render with verboseMode: false, assert 'verbose' not in output
});

Reviewed by glm-5.1 via Qwen Code /review

import { useVerboseMode } from '../contexts/VerboseModeContext.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';

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,
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -178,6 +180,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
isFocused={isFocused}
activeShellPtyId={activeShellPtyId}
embeddedShellFocused={embeddedShellFocused}
isUserInitiated={itemForDisplay.isUserInitiated}
/>
)}
{itemForDisplay.type === 'compression' && (
Expand Down
37 changes: 22 additions & 15 deletions packages/cli/src/ui/components/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on narrow terminal > complete-footer-narrow 1`] = `" ? for shortcuts 0.1% used"`;
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on narrow terminal > complete-footer-narrow 1`] = `" ? for shortcuts 0.1% used | verbose"`;

exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on wide terminal > complete-footer-wide 1`] = `" ? for shortcuts 0.1% context used"`;
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on wide terminal > complete-footer-wide 1`] = `" ? for shortcuts 0.1% context used | verbose"`;
Loading
Loading