diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 7499a8c683e..b13da27fa31 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -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', @@ -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 }], diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index d2cf5081c76..e6a9642606d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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: false, + description: + 'Show full tool output and thinking in verbose mode (toggle with ctrl+o).', + showInDialog: false, + }, }, }, diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index cb3229a2bed..aaf814ee179 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1948,4 +1948,5 @@ 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: 'verbose', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 3178ea533d8..544dfb45c26 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1989,4 +1989,5 @@ 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', }; diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index ac5f5911166..4b4bb53fb0e 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -1441,4 +1441,5 @@ export default { 'Rawモードが利用できません。インタラクティブターミナルで実行してください。', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(↑ ↓ 矢印キーで移動、Enter で選択、Ctrl+C で終了)\n', + verbose: 'verbose', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 993cd8d8c1d..928f198a27f 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1938,4 +1938,5 @@ 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: 'verbose', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index bb7e8968f76..ee0cfe69baa 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1945,4 +1945,5 @@ export default { 'Raw-режим недоступен. Пожалуйста, запустите в интерактивном терминале.', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(↑ ↓ стрелки для навигации, Enter для выбора, Ctrl+C для выхода)\n', + verbose: 'verbose', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index ad755b721e8..56b747dcf3e 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1795,4 +1795,5 @@ export default { '原始模式不可用。请在交互式终端中运行。', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(使用 ↑ ↓ 箭头导航,Enter 选择,Ctrl+C 退出)\n', + verbose: '详细模式', }; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 37dc325180f..b4b3a4dd9f4 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -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'; @@ -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, @@ -963,6 +969,13 @@ export const AppContainer = (props: AppContainerProps) => { const [showToolDescriptions, setShowToolDescriptions] = useState(false); + const [verboseMode, setVerboseMode] = useState( + settings.merged.ui?.verboseMode ?? false, + ); + const [frozenSnapshot, setFrozenSnapshot] = useState< + HistoryItemWithoutId[] | null + >(null); + const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false); const ctrlCTimerRef = useRef(null); const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false); @@ -977,6 +990,12 @@ export const AppContainer = (props: AppContainerProps) => { const [showEscapePrompt, setShowEscapePrompt] = useState(false); const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false); + useEffect(() => { + if (streamingState === StreamingState.Idle) { + setFrozenSnapshot(null); + } + }, [streamingState]); + const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } = useFolderTrust(settings, setIsTrustedFolder); const { @@ -1347,6 +1366,16 @@ 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(); + if (newValue && streamingState !== StreamingState.Idle) { + setFrozenSnapshot([...pendingHistoryItems]); + } else { + setFrozenSnapshot(null); + } } }, [ @@ -1375,8 +1404,13 @@ export const AppContainer = (props: AppContainerProps) => { btwItem, setBtwItem, cancelBtw, - settings.merged.general?.debugKeystrokeLogging, + settings, isAuthenticating, + verboseMode, + setVerboseMode, + setFrozenSnapshot, + pendingHistoryItems, + refreshStatic, ], ); @@ -1465,11 +1499,6 @@ export const AppContainer = (props: AppContainerProps) => { sessionStats, }); - const pendingHistoryItems = useMemo( - () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], - [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], - ); - const uiState: UIState = useMemo( () => ({ history: historyManager.history, @@ -1807,9 +1836,11 @@ export const AppContainer = (props: AppContainerProps) => { startupWarnings: props.startupWarnings || [], }} > - - - + + + + + diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index af81f6a5d7c..0305d0320fd 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -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: {t('verbose')}, + }); + } return ( = ({ ? 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 = ({ contentWidth={contentWidth} /> )} - {itemForDisplay.type === 'gemini_thought' && ( + {verboseMode && itemForDisplay.type === 'gemini_thought' && ( = ({ contentWidth={contentWidth} /> )} - {itemForDisplay.type === 'gemini_thought_content' && ( + {verboseMode && itemForDisplay.type === 'gemini_thought_content' && ( { const { version } = useAppContext(); const uiState = useUIState(); + const { frozenSnapshot } = useVerboseMode(); const { pendingHistoryItems, terminalWidth, @@ -57,21 +59,26 @@ export const MainContent = () => { - {pendingHistoryItems.map((item, i) => ( - - ))} + {(frozenSnapshot ?? pendingHistoryItems).map((item, i) => { + const isFrozen = frozenSnapshot !== null; + return ( + + ); + })} diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index e5f84660172..fc624448f56 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -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( - - - {ui} - - , + + + + {ui} + + + , ); }; @@ -143,6 +147,18 @@ describe('', () => { expect(output).toContain('MockMarkdown:Test result'); }); + it('hides result output in compact mode (verboseMode=false)', () => { + const { lastFrame } = renderWithContext( + , + 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( diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index 3da9f0edd3d..68bb2b45831 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -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 = ({ // 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 ( @@ -344,44 +349,44 @@ export const ToolMessage: React.FC = ({ )} {emphasis === 'high' && } - {displayRenderer.type !== 'none' && ( + {effectiveDisplayRenderer.type !== 'none' && ( - {displayRenderer.type === 'todo' && ( - + {effectiveDisplayRenderer.type === 'todo' && ( + )} - {displayRenderer.type === 'plan' && ( + {effectiveDisplayRenderer.type === 'plan' && ( )} - {displayRenderer.type === 'task' && config && ( + {effectiveDisplayRenderer.type === 'task' && config && ( )} - {displayRenderer.type === 'diff' && ( + {effectiveDisplayRenderer.type === 'diff' && ( )} - {displayRenderer.type === 'ansi' && ( + {effectiveDisplayRenderer.type === 'ansi' && ( )} - {displayRenderer.type === 'string' && ( + {effectiveDisplayRenderer.type === 'string' && ( ({ + verboseMode: false, + frozenSnapshot: null, +}); + +export const useVerboseMode = (): VerboseModeContextType => + useContext(VerboseModeContext); + +export const VerboseModeProvider = VerboseModeContext.Provider; diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index 8961f9ff702..856c300c3d9 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -60,6 +60,7 @@ describe('keyMatchers', () => { [Command.EXIT]: (key: Key) => key.ctrl && key.name === 'd', [Command.SHOW_MORE_LINES]: (key: Key) => key.ctrl && key.name === 's', [Command.RETRY_LAST]: (key: Key) => key.ctrl && key.name === 'y', + [Command.TOGGLE_VERBOSE_MODE]: (key: Key) => key.ctrl && key.name === 'o', [Command.REVERSE_SEARCH]: (key: Key) => key.ctrl && key.name === 'r', [Command.SUBMIT_REVERSE_SEARCH]: (key: Key) => key.name === 'return' && !key.ctrl, @@ -258,6 +259,11 @@ describe('keyMatchers', () => { positive: [createKey('y', { ctrl: true })], negative: [createKey('y'), createKey('r', { ctrl: true })], }, + { + command: Command.TOGGLE_VERBOSE_MODE, + positive: [createKey('o', { ctrl: true })], + negative: [createKey('o'), createKey('p', { ctrl: true })], + }, // Shell commands { diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index c7f53048e1b..4d6f1edc3ec 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -194,6 +194,11 @@ "description": "The last time the feedback dialog was shown.", "type": "number", "default": 0 + }, + "verboseMode": { + "description": "Show full tool output and thinking in verbose mode (toggle with ctrl+o).", + "type": "boolean", + "default": false } } },