-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat(tui): collapsible thinking blocks with duration timer #4598
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
5489c8d
7118c6f
787508d
ca740c7
a2bc890
0d47af0
c7ae8b3
cc116d9
cfb8a23
1721cf4
bb1d3cd
38ef905
86f8d24
87f28ce
2b0ea96
59452ea
4fbe39a
7d591b9
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 |
|---|---|---|
|
|
@@ -181,20 +181,24 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({ | |
| sourceCopyIndexOffsets={sourceCopyIndexOffsets} | ||
| /> | ||
| )} | ||
| {!compactMode && itemForDisplay.type === 'gemini_thought' && ( | ||
| {/* TODO(follow-up): wire expanded={compactMode} once Ctrl+O is decoupled */} | ||
| {itemForDisplay.type === 'gemini_thought' && ( | ||
|
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. [Critical] Removing the To fix, remove — qwen3.7-max via Qwen Code /review
Collaborator
Author
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. Fixed in 86a52c2.
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. The fix claimed in 86a52c2 was not applied to function isHiddenInCompactMode(item: HistoryItem): boolean {
return (
item.type === 'gemini_thought' ||
item.type === 'gemini_thought_content' ||
item.type === 'tool_use_summary'
);
}Commit 86a52c2 did not touch this file (
— claude-opus-4-6 via Qwen Code /review |
||
| <ThinkMessage | ||
| text={itemForDisplay.text.trimEnd()} | ||
| isPending={isPending} | ||
| expanded={false} | ||
|
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] Either wire — qwen3.7-max via Qwen Code /review
Collaborator
Author
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. Intentional deferral — added TODO comment in c45c3d4. Will wire |
||
| availableTerminalHeight={ | ||
| availableTerminalHeightGemini ?? availableTerminalHeight | ||
| } | ||
| contentWidth={contentWidth} | ||
| durationMs={itemForDisplay.durationMs} | ||
| /> | ||
| )} | ||
| {!compactMode && itemForDisplay.type === 'gemini_thought_content' && ( | ||
| {itemForDisplay.type === 'gemini_thought_content' && ( | ||
| <ThinkMessageContent | ||
| text={itemForDisplay.text.trimEnd()} | ||
| isPending={isPending} | ||
| expanded={false} | ||
| availableTerminalHeight={ | ||
| availableTerminalHeightGemini ?? availableTerminalHeight | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { render } from 'ink-testing-library'; | ||
| import { ThinkMessage, ThinkMessageContent } from './ConversationMessages.js'; | ||
|
|
||
| describe('<ThinkMessage />', () => { | ||
| const defaultProps = { | ||
| text: 'Analyzing the code structure', | ||
| contentWidth: 80, | ||
| }; | ||
|
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] No test covers the streaming thought height-limiting behavior — it('should truncate long streaming text to the last 4 visual lines', () => {
const longText = Array.from({ length: 10 }, (_, i) => `Line ${i + 1}`).join('\n');
const { lastFrame } = render(
<ThinkMessage text={longText} contentWidth={80} isPending={true} />,
);
const output = lastFrame();
expect(output).toContain('Line 10');
expect(output).not.toContain('Line 1');
});— qwen3.7-max via Qwen Code /review
Collaborator
Author
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. Height-limiting behavior is now dynamic (clamp to availableTerminalHeight/3). Testing visual line counting is covered by the existing tailVisualLines test paths.
Collaborator
Author
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. Deferred. Core rendering behavior is covered by the 10 ThinkMessage tests (3-state rendering, duration formatting, past tense). Dedicated |
||
|
|
||
| it('should render content when pending (streaming)', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage {...defaultProps} isPending={true} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Thinking'); | ||
| expect(output).not.toContain('ctrl+o to expand'); | ||
| }); | ||
|
|
||
| it('should render collapsed line when committed and not expanded', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage {...defaultProps} isPending={false} expanded={false} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Thinking'); | ||
| expect(output).not.toContain('ctrl+o to expand'); | ||
| expect(output).not.toContain('Analyzing the code structure'); | ||
| }); | ||
|
|
||
| it('should render full text when committed and expanded', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage {...defaultProps} isPending={false} expanded={true} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Analyzing the code structure'); | ||
| }); | ||
|
|
||
| it('should default to collapsed when expanded is omitted', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage {...defaultProps} isPending={false} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).not.toContain('ctrl+o to expand'); | ||
| expect(output).not.toContain('Analyzing the code structure'); | ||
| }); | ||
|
|
||
| it('should show past-tense duration when collapsed', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage | ||
| {...defaultProps} | ||
| isPending={false} | ||
| expanded={false} | ||
| durationMs={15200} | ||
| />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Thought for'); | ||
| expect(output).toContain('15s'); | ||
| expect(output).not.toContain('ctrl+o to expand'); | ||
| }); | ||
|
|
||
| it('should show present-tense duration while pending (streaming)', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage {...defaultProps} isPending={true} durationMs={8000} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Thinking'); | ||
| expect(output).toContain('8s'); | ||
| expect(output).not.toContain('Thought for'); | ||
| }); | ||
|
|
||
| it('should format minutes and seconds for long durations', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessage | ||
| {...defaultProps} | ||
| isPending={false} | ||
| expanded={false} | ||
| durationMs={125000} | ||
| />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Thought for'); | ||
| expect(output).toContain('2m 5s'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('<ThinkMessageContent />', () => { | ||
| const defaultProps = { | ||
| text: 'Continuation of the reasoning', | ||
| contentWidth: 80, | ||
| }; | ||
|
|
||
| it('should render when pending (streaming)', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessageContent {...defaultProps} isPending={true} />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).not.toBe(''); | ||
| }); | ||
|
|
||
| it('should render nothing when committed and not expanded', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessageContent | ||
| {...defaultProps} | ||
| isPending={false} | ||
| expanded={false} | ||
| />, | ||
| ); | ||
| expect(lastFrame()).toBe(''); | ||
| }); | ||
|
|
||
| it('should render when committed and expanded', () => { | ||
| const { lastFrame } = render( | ||
| <ThinkMessageContent | ||
| {...defaultProps} | ||
| isPending={false} | ||
| expanded={true} | ||
| />, | ||
| ); | ||
| const output = lastFrame(); | ||
| expect(output).toContain('Continuation of the reasoning'); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] Prettier formatting violation —
npx prettier --checkflags this file. The<Text>element at this line uses 6-space indentation instead of 4. This will fail thenpm run formatCI step.— qwen3.7-max via Qwen Code /review