Skip to content
Draft
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
42 changes: 42 additions & 0 deletions static/app/components/core/chat/thinkingBlock.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,48 @@ describe('ThinkingBlock', () => {
expect(screen.getByText('inner content')).not.toBeVisible();
});

it('can be manually collapsed while thinking is active', async () => {
jest.useRealTimers();
const start = new Date();

render(
<ThinkingBlock title="Thinking" startTime={start}>
<div>inner content</div>
</ThinkingBlock>
);

expect(screen.getByText('inner content')).toBeVisible();

await userEvent.click(screen.getByText('Thinking'));
expect(screen.getByText('inner content')).not.toBeVisible();
});

it('auto-collapses when thinking completes even if user re-expanded', async () => {
jest.useRealTimers();
const start = new Date();

const {rerender} = render(
<ThinkingBlock title="Thinking" startTime={start}>
<div>inner content</div>
</ThinkingBlock>
);

// collapse then re-expand while still active
await userEvent.click(screen.getByText('Thinking'));
expect(screen.getByText('inner content')).not.toBeVisible();
await userEvent.click(screen.getByText('Thinking'));
expect(screen.getByText('inner content')).toBeVisible();

// thinking completes → auto-collapse
rerender(
<ThinkingBlock title="Thinking" startTime={start} endTime={new Date()}>
<div>inner content</div>
</ThinkingBlock>
);

expect(screen.getByText('inner content')).not.toBeVisible();
});

it('can be manually toggled after collapsing', async () => {
jest.useRealTimers();
const start = new Date();
Expand Down
13 changes: 10 additions & 3 deletions static/app/components/core/chat/thinkingBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,27 @@ interface ThinkingBlockProps {
export function ThinkingBlock({title, startTime, endTime, children}: ThinkingBlockProps) {
const elapsed = useElapsedTime(startTime, endTime);
const isActive = !endTime;
const [userExpanded, setUserExpanded] = useState(false);
// ponytail: null = no user interaction, falls through to isActive default
const [override, setOverride] = useState<boolean | null>(null);

useEffect(() => {
if (!isActive) {
setOverride(null);
}
}, [isActive]);

const titleRef = useRef<HTMLSpanElement>(null);
// Strip trailing punctuation/whitespace so the animated ellipsis isn't
// doubled up when the title already ends in "." or "…".
const baseTitle = title.replace(/[.…\s]+$/u, '');
useTextDecodeAnimation(titleRef, baseTitle);

const isExpanded = isActive || userExpanded;
const isExpanded = override ?? isActive;

return (
<Disclosure
expanded={isExpanded}
onExpandedChange={setUserExpanded}
onExpandedChange={setOverride}
size="sm"
variant="outline"
flex={1}
Expand Down
Loading