Skip to content

Commit 43a2654

Browse files
authored
Merge branch 'main' into fix/model-picker-effort-default
2 parents 902027e + b69205f commit 43a2654

4 files changed

Lines changed: 36 additions & 37 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pythoughts/pythinker-code": patch
3+
---
4+
5+
Show only the animated thinking indicator while the model thinks; the streamed thinking text no longer appears in the transcript unless expanded with Ctrl+O.

apps/pythinker-code/src/tui/components/messages/thinking.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Supports expand/collapse via Ctrl+O (shared with tool output).
66
*/
77

8-
import { Markdown, truncateToWidth, type Component, type TUI } from '@earendil-works/pi-tui';
8+
import { Markdown, type Component, type TUI } from '@earendil-works/pi-tui';
99

1010
import {
1111
formatThinkingSpinnerLabel,
@@ -85,14 +85,11 @@ export class ThinkingComponent implements Component {
8585
}
8686

8787
render(width: number): string[] {
88-
const contentWidth = Math.max(1, width - MESSAGE_INDENT.length);
89-
const contentLines = this.text.length > 0 ? this.textComponent.render(contentWidth) : [''];
90-
88+
// Collapsed thinking renders no body text: while live only the spinner
89+
// header shows (it sits directly above the prompt as the newest entry),
90+
// and a finalized block disappears from the transcript entirely.
91+
// Ctrl+O (expand) opts back into the full text.
9192
if (this.mode === 'live') {
92-
const visibleLines =
93-
contentLines.length > THINKING_PREVIEW_LINES
94-
? contentLines.slice(contentLines.length - THINKING_PREVIEW_LINES)
95-
: contentLines;
9693
const spinner = currentTheme.fg(
9794
'primary',
9895
`${BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0]} `,
@@ -102,29 +99,29 @@ export class ThinkingComponent implements Component {
10299
shimmerToken: 'primaryShimmer',
103100
bandHalfWidth: 4,
104101
});
102+
if (!this.expanded) return ['', spinner + label];
103+
const contentLines = this.renderContent(width);
104+
const visibleLines =
105+
contentLines.length > THINKING_PREVIEW_LINES
106+
? contentLines.slice(contentLines.length - THINKING_PREVIEW_LINES)
107+
: contentLines;
105108
return ['', spinner + label, ...visibleLines.map((line) => MESSAGE_INDENT + line)];
106109
}
107110

111+
if (!this.expanded) return [];
112+
113+
const contentLines = this.renderContent(width);
108114
const rendered: string[] = [''];
109115
for (let i = 0; i < contentLines.length; i++) {
110116
const p = i === 0 && this.showMarker ? currentTheme.fg('textDim', STATUS_BULLET) : MESSAGE_INDENT;
111117
rendered.push(p + contentLines[i]);
112118
}
119+
return rendered;
120+
}
113121

114-
if (this.expanded || contentLines.length <= THINKING_PREVIEW_LINES) {
115-
return rendered;
116-
}
117-
118-
// Leading blank + first PREVIEW_LINES content lines + hint line.
119-
const truncated = rendered.slice(0, 1 + THINKING_PREVIEW_LINES);
120-
const remaining = contentLines.length - THINKING_PREVIEW_LINES;
121-
const hint = `... (${String(remaining)} more lines, ctrl+o to expand)`;
122-
const indentWidth = Math.min(MESSAGE_INDENT.length, Math.max(0, width));
123-
const hintWidth = Math.max(0, width - indentWidth);
124-
truncated.push(
125-
' '.repeat(indentWidth) + currentTheme.dim(truncateToWidth(hint, hintWidth, '…')),
126-
);
127-
return truncated;
122+
private renderContent(width: number): string[] {
123+
const contentWidth = Math.max(1, width - MESSAGE_INDENT.length);
124+
return this.text.length > 0 ? this.textComponent.render(contentWidth) : [''];
128125
}
129126

130127
private startSpinner(): void {

apps/pythinker-code/test/tui/components/messages/thinking.test.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ describe('getThinkingSpinnerLabel', () => {
3232
});
3333

3434
describe('ThinkingComponent', () => {
35-
it('shows the live spinner header before thinking content', () => {
35+
it('shows only the live spinner header while collapsed', () => {
3636
const component = new ThinkingComponent('working it out', true, 'live');
3737
const out = strip(component.render(80).join('\n'));
3838
const label = formatThinkingSpinnerLabel();
3939

4040
expect(out).toContain(`⠋ ${label}`);
4141
expect(out).not.toContain(` ⠋ ${label}`);
4242
expect(out).not.toContain(`${STATUS_BULLET}⠋`);
43-
expect(out).toContain(' working it out');
43+
expect(out).not.toContain('working it out');
4444
});
4545

4646
it('uses the primary activity color while thinking is live', () => {
@@ -57,8 +57,9 @@ describe('ThinkingComponent', () => {
5757
}
5858
});
5959

60-
it('keeps live thinking height-limited to the tail', () => {
60+
it('keeps expanded live thinking height-limited to the tail', () => {
6161
const component = new ThinkingComponent(longThinking, true, 'live');
62+
component.setExpanded(true);
6263
const out = strip(component.render(80).join('\n'));
6364

6465
expect(out).not.toContain('line1');
@@ -116,17 +117,12 @@ describe('ThinkingComponent', () => {
116117
}
117118
});
118119

119-
it('finalizes in place into a collapsed preview', () => {
120+
it('finalizes in place into nothing while collapsed', () => {
120121
const component = new ThinkingComponent(longThinking, true, 'live');
121122

122123
component.finalize();
123124

124-
const out = strip(component.render(80).join('\n'));
125-
expect(out).toContain('line1');
126-
expect(out).toContain('line2');
127-
expect(out).not.toContain('line3');
128-
expect(out).not.toContain('line4');
129-
expect(out).toContain('... (5 more lines, ctrl+o to expand)');
125+
expect(component.render(80)).toEqual([]);
130126
});
131127

132128
it('expands and collapses after finalization', () => {
@@ -135,18 +131,18 @@ describe('ThinkingComponent', () => {
135131

136132
component.setExpanded(true);
137133
const expanded = strip(component.render(80).join('\n'));
134+
expect(expanded).toContain('line1');
138135
expect(expanded).toContain('line7');
139136
expect(expanded).not.toContain('ctrl+o to expand');
140137

141138
component.setExpanded(false);
142-
const collapsed = strip(component.render(80).join('\n'));
143-
expect(collapsed).not.toContain('line7');
144-
expect(collapsed).toContain('ctrl+o to expand');
139+
expect(component.render(80)).toEqual([]);
145140
});
146141

147-
it('keeps the finalized truncation footer within the requested render width', () => {
142+
it('keeps expanded finalized lines within the requested render width', () => {
148143
const component = new ThinkingComponent(longThinking, true, 'live');
149144
component.finalize();
145+
component.setExpanded(true);
150146

151147
for (const line of component.render(37)) {
152148
expect(visibleWidth(line)).toBeLessThanOrEqual(37);

apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5891,7 +5891,8 @@ command = "vim"
58915891

58925892
expect(driver.streamingUI.hasActiveThinkingComponent()).toBe(true);
58935893
expect(driver.state.appState.streamingPhase).toBe('thinking');
5894-
expect(stripSgr(renderTranscript(driver))).toContain('visible reasoning');
5894+
// Collapsed live thinking renders only the spinner header, never the text.
5895+
expect(stripSgr(renderTranscript(driver))).not.toContain('visible reasoning');
58955896
});
58965897

58975898
it('does not create a thinking component for whitespace-only replay content', async () => {

0 commit comments

Comments
 (0)