-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(tui): add thinking block viewer with Alt+T expand/collapse #5627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7199148
2e5e7ea
2c5734e
51d51e5
1e0e045
35aeb44
b0a078a
a02ec91
1a6114b
78c5562
b5d2142
7290242
1f50cbb
bfe8547
3096073
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -134,6 +134,7 @@ import { | |||||||||||||||||||
| useVimModeActions, | ||||||||||||||||||||
| } from './contexts/VimModeContext.js'; | ||||||||||||||||||||
| import { CompactModeProvider } from './contexts/CompactModeContext.js'; | ||||||||||||||||||||
| import { ThoughtExpandedProvider } from './contexts/ThoughtExpandedContext.js'; | ||||||||||||||||||||
| import { useTerminalSize } from './hooks/useTerminalSize.js'; | ||||||||||||||||||||
| import { calculatePromptWidths } from './components/InputPrompt.js'; | ||||||||||||||||||||
| import { useStdin, useStdout } from 'ink'; | ||||||||||||||||||||
|
|
@@ -197,6 +198,11 @@ import { | |||||||||||||||||||
| type RenderMode, | ||||||||||||||||||||
| } from './contexts/RenderModeContext.js'; | ||||||||||||||||||||
| import { TerminalOutputProvider } from './contexts/TerminalOutputContext.js'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| ThinkingViewerProvider, | ||||||||||||||||||||
| type ThinkingViewerData, | ||||||||||||||||||||
| } from './contexts/ThinkingViewerContext.js'; | ||||||||||||||||||||
| import { ThinkingViewer } from './components/ThinkingViewer.js'; | ||||||||||||||||||||
| import { useAgentViewState } from './contexts/AgentViewContext.js'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| useBackgroundTaskViewState, | ||||||||||||||||||||
|
|
@@ -473,6 +479,19 @@ export const AppContainer = (props: AppContainerProps) => { | |||||||||||||||||||
|
|
||||||||||||||||||||
| const [userMessages, setUserMessages] = useState<string[]>([]); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Thinking viewer overlay state | ||||||||||||||||||||
| const [thinkingViewerData, setThinkingViewerData] = | ||||||||||||||||||||
| useState<ThinkingViewerData | null>(null); | ||||||||||||||||||||
| const openThinkingViewer = useCallback((data: ThinkingViewerData) => { | ||||||||||||||||||||
| setThinkingViewerData(data); | ||||||||||||||||||||
| }, []); | ||||||||||||||||||||
| const closeThinkingViewer = useCallback(() => { | ||||||||||||||||||||
| setThinkingViewerData(null); | ||||||||||||||||||||
| }, []); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Alt+T inline expansion toggle for thinking blocks | ||||||||||||||||||||
| const [thoughtExpanded, setThoughtExpanded] = useState(false); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Terminal and layout hooks | ||||||||||||||||||||
| const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize(); | ||||||||||||||||||||
| const { stdin, setRawMode } = useStdin(); | ||||||||||||||||||||
|
|
@@ -3101,6 +3120,23 @@ export const AppContainer = (props: AppContainerProps) => { | |||||||||||||||||||
| debugLogger.debug('[DEBUG] Keystroke:', JSON.stringify(key)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ThinkingViewer owns all input while open. | ||||||||||||||||||||
| // Ctrl+C / Ctrl+D close the viewer and fall through to quit/exit. | ||||||||||||||||||||
| if (thinkingViewerData) { | ||||||||||||||||||||
| if (keyMatchers[Command.QUIT](key) || keyMatchers[Command.EXIT](key)) { | ||||||||||||||||||||
| closeThinkingViewer(); | ||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] After Consider adding
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||
| } else { | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Alt+T: toggle inline expansion of thinking blocks. | ||||||||||||||||||||
| if (keyMatchers[Command.TOGGLE_THINKING_EXPANDED](key)) { | ||||||||||||||||||||
| setThoughtExpanded((prev) => !prev); | ||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion]
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||
| refreshStatic(); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (keyMatchers[Command.QUIT](key)) { | ||||||||||||||||||||
| if (isAuthenticating) { | ||||||||||||||||||||
| return; | ||||||||||||||||||||
|
|
@@ -3361,6 +3397,9 @@ export const AppContainer = (props: AppContainerProps) => { | |||||||||||||||||||
| handleDoubleEscRewind, | ||||||||||||||||||||
| vimEnabled, | ||||||||||||||||||||
| vimMode, | ||||||||||||||||||||
| thinkingViewerData, | ||||||||||||||||||||
| closeThinkingViewer, | ||||||||||||||||||||
| setThoughtExpanded, | ||||||||||||||||||||
| ], | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -3900,6 +3939,11 @@ export const AppContainer = (props: AppContainerProps) => { | |||||||||||||||||||
| [renderMode, setRenderMode], | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const thinkingViewerValue = useMemo( | ||||||||||||||||||||
| () => ({ openThinkingViewer }), | ||||||||||||||||||||
| [openThinkingViewer], | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return ( | ||||||||||||||||||||
| <UIStateContext.Provider value={uiState}> | ||||||||||||||||||||
| <UIActionsContext.Provider value={uiActions}> | ||||||||||||||||||||
|
|
@@ -3911,13 +3955,25 @@ export const AppContainer = (props: AppContainerProps) => { | |||||||||||||||||||
| }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| <CompactModeProvider value={compactModeValue}> | ||||||||||||||||||||
| <RenderModeProvider value={renderModeValue}> | ||||||||||||||||||||
| <TerminalOutputProvider value={writeRaw}> | ||||||||||||||||||||
| <ShellFocusContext.Provider value={isFocused}> | ||||||||||||||||||||
| <App /> | ||||||||||||||||||||
| </ShellFocusContext.Provider> | ||||||||||||||||||||
| </TerminalOutputProvider> | ||||||||||||||||||||
| </RenderModeProvider> | ||||||||||||||||||||
| <ThoughtExpandedProvider value={thoughtExpanded}> | ||||||||||||||||||||
| <RenderModeProvider value={renderModeValue}> | ||||||||||||||||||||
| <TerminalOutputProvider value={writeRaw}> | ||||||||||||||||||||
| <ThinkingViewerProvider value={thinkingViewerValue}> | ||||||||||||||||||||
| <ShellFocusContext.Provider value={isFocused}> | ||||||||||||||||||||
| {thinkingViewerData ? ( | ||||||||||||||||||||
| <ThinkingViewer | ||||||||||||||||||||
| data={thinkingViewerData} | ||||||||||||||||||||
| onClose={closeThinkingViewer} | ||||||||||||||||||||
| useAlternateScreen={!useTerminalBuffer} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| ) : ( | ||||||||||||||||||||
| <App /> | ||||||||||||||||||||
| )} | ||||||||||||||||||||
| </ShellFocusContext.Provider> | ||||||||||||||||||||
| </ThinkingViewerProvider> | ||||||||||||||||||||
| </TerminalOutputProvider> | ||||||||||||||||||||
| </RenderModeProvider> | ||||||||||||||||||||
| </ThoughtExpandedProvider> | ||||||||||||||||||||
| </CompactModeProvider> | ||||||||||||||||||||
| </AppContext.Provider> | ||||||||||||||||||||
| </ConfigContext.Provider> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import type { FC, ReactNode } from 'react'; | ||
| import { useEffect } from 'react'; | ||
| import { Box } from 'ink'; | ||
| import { useTerminalOutput } from '../contexts/TerminalOutputContext.js'; | ||
| import { useTerminalSize } from '../hooks/useTerminalSize.js'; | ||
|
|
||
| const ENTER_ALT_SCREEN = '\x1b[?1049h'; | ||
| const EXIT_ALT_SCREEN = '\x1b[?1049l'; | ||
| const CLEAR_SCREEN = '\x1b[2J\x1b[H'; | ||
| const HIDE_CURSOR = '\x1b[?25l'; | ||
| const SHOW_CURSOR = '\x1b[?25h'; | ||
|
|
||
| interface AlternateScreenProps { | ||
| children: ReactNode; | ||
| /** Skip escape writes when the root Ink renderer already owns the alt screen (VP mode). */ | ||
| disabled?: boolean; | ||
| } | ||
|
|
||
| export const AlternateScreen: FC<AlternateScreenProps> = ({ | ||
| children, | ||
| disabled, | ||
| }) => { | ||
| const writeRaw = useTerminalOutput(); | ||
| const { rows } = useTerminalSize(); | ||
|
|
||
| useEffect(() => { | ||
| if (disabled) return; | ||
| writeRaw(ENTER_ALT_SCREEN + CLEAR_SCREEN + HIDE_CURSOR); | ||
|
chiga0 marked this conversation as resolved.
|
||
| const onExit = () => writeRaw(SHOW_CURSOR + EXIT_ALT_SCREEN); | ||
| process.on('exit', onExit); | ||
| return () => { | ||
| process.removeListener('exit', onExit); | ||
| writeRaw(SHOW_CURSOR + EXIT_ALT_SCREEN); | ||
| }; | ||
| }, [writeRaw, disabled]); | ||
|
|
||
| return ( | ||
| <Box flexDirection="column" height={rows}> | ||
| {children} | ||
| </Box> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.