Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const chatInputWorkspaceStripAppearanceDescriptor: AppearanceSurfaceDescr
// `harness` / `runtime` / `actions` grouping went with the conditional grid.
parts: [
{ id: 'root' }, { id: 'context' }, { id: 'next' },
{ id: 'workspace' }, { id: 'branch' },
{ id: 'workspace' }, { id: 'workspaceMenu' },
{ id: 'workspaceOption' }, { id: 'branch' }, { id: 'divider' },
{ id: 'permission' }, { id: 'permissionMenu' },
{ id: 'permissionOptions' }, { id: 'permissionOptionRow' },
{ id: 'permissionOption' }, { id: 'permissionOptionTrailing' },
Expand All @@ -15,6 +16,7 @@ export const chatInputWorkspaceStripAppearanceDescriptor: AppearanceSurfaceDescr
states: [
{ id: 'open', selector: { kind: 'self', suffix: '[data-bf-state~="open"]' } },
{ id: 'selected', selector: { kind: 'self', suffix: '[data-bf-state~="selected"]' } },
{ id: 'active', selector: { kind: 'self', suffix: '[data-bf-state~="active"]' } },
{ id: 'armed', selector: { kind: 'self', suffix: '[data-bf-state~="armed"]' } },
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,12 @@ describe('FileOperationToolCard', () => {
});

expect(container.querySelector('[data-testid="chat-file-change-preview"]')).not.toBeNull();
expect(mocks.codePreviewProps).toHaveLength(1);
expect(mocks.codePreviewProps[0]).toMatchObject({
expect(mocks.codePreviewProps).toHaveLength(2);
expect(mocks.codePreviewProps.at(-1)).toMatchObject({
isStreaming: false,
maxHeight: 88,
});
expect(mocks.inlineDiffPreviewProps).toHaveLength(0);

await act(async () => {
root.render(
Expand All @@ -908,6 +909,7 @@ describe('FileOperationToolCard', () => {

// Auto-collapse animates closed; wait for SmoothHeightCollapse to unmount children.
expect(container.querySelector('[data-testid="chat-file-change-card"]')?.getAttribute('data-expanded')).toBe('false');
expect(mocks.inlineDiffPreviewProps).toHaveLength(0);
await act(async () => {
await new Promise((resolve) => {
window.setTimeout(resolve, 350);
Expand Down
20 changes: 17 additions & 3 deletions src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -509,16 +509,30 @@ export const FileOperationToolCard: React.FC<FileOperationToolCardProps> = ({
}, [sessionId, toolCall?.id, status, isFailed]);

const isLoading = status === 'preparing' || status === 'streaming' || status === 'running';
/*
* Auto-managed completed cards must keep their compact streaming preview
* from the first completed render through the collapse commit. Waiting for
* the grace-period layout effect to set its state briefly renders the large
* diff preview, and follow-output can treat that transient height as output.
* A manually expanded card marks itself as user-owned and still gets the
* full diff preview.
*/
const keepAutoCompletionPreview =
status === 'completed' &&
!isFailed &&
!userToggledContentRef.current;
const keepCompactCompletionPreview =
retainLiveCompletionPreview || keepAutoCompletionPreview;
const shouldUseExpandedDiffPreviewHeight =
status === 'completed' &&
isContentExpanded &&
!retainLiveCompletionPreview;
!keepCompactCompletionPreview;
const keepLiveEditPreview =
retainLiveCompletionPreview &&
keepCompactCompletionPreview &&
toolItem.toolName === 'Edit' &&
Boolean(newStringContent);
const keepLiveWritePreview =
retainLiveCompletionPreview &&
keepCompactCompletionPreview &&
toolItem.toolName === 'Write' &&
Boolean(contentPreview);
const previewVariant = useMemo(() => {
Expand Down
18 changes: 15 additions & 3 deletions src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('createTodoRenderItems', () => {
});
});

describe('TodoWriteDisplay automatic collapse', () => {
describe('TodoWriteDisplay expansion', () => {
let container: HTMLDivElement;
let root: Root;

Expand Down Expand Up @@ -98,19 +98,31 @@ describe('TodoWriteDisplay automatic collapse', () => {
vi.restoreAllMocks();
});

it('lets completed todo content collapse through normal layout state', () => {
it('stays collapsed during streaming until the user expands it', () => {
vi.useFakeTimers();
act(() => {
root.render(<TodoWriteDisplay toolItem={createTodoWriteItem('pending')} config={config} />);
});
expect(container.querySelector('.todo-expanded-body')).not.toBeNull();
expect(container.querySelector('.todo-expanded-body')).toBeNull();

act(() => {
root.render(<TodoWriteDisplay toolItem={createTodoWriteItem('in_progress')} config={config} />);
});
expect(container.querySelector('.todo-expanded-body')).toBeNull();

act(() => {
container.querySelector<HTMLElement>('[data-testid="todo-write-toggle"]')?.click();
});
expect(container.querySelector('.todo-expanded-body')).not.toBeNull();

act(() => {
root.render(<TodoWriteDisplay toolItem={createTodoWriteItem('pending')} config={config} />);
});
expect(container.querySelector('.todo-expanded-body')).not.toBeNull();

act(() => {
container.querySelector<HTMLElement>('[data-testid="todo-write-toggle"]')?.click();
});
act(() => {
vi.advanceTimersByTime(FLOWCHAT_COLLAPSE_DURATION_MS);
});
Expand Down
35 changes: 4 additions & 31 deletions src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Tool card for TodoWrite.
*/

import React, { useState, useMemo, useCallback, useLayoutEffect } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { ListTodo, CheckCircle2, Circle, XCircle } from 'lucide-react';
import { TaskRunningIndicator } from '../../component-library';
import { useTranslation } from 'react-i18next';
Expand All @@ -23,7 +23,7 @@ export const TodoWriteDisplay: React.FC<ToolCardProps> = ({
const { t } = useTranslation('flow-chat');
const { status, toolResult, partialParams, isParamsStreaming } = toolItem;

const [expandedState, setExpandedState] = useState<boolean | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const toolId = toolItem.id;
const { cardRootRef, applyExpandedState } = useToolCardHeightContract({
toolId,
Expand Down Expand Up @@ -73,32 +73,6 @@ export const TodoWriteDisplay: React.FC<ToolCardProps> = ({
? { status: 'completed' as const, defaultIcon: 'status' as const }
: { status, defaultIcon: 'tool' as const };

const desiredAutomaticExpanded = useMemo(() => {
return inProgressTasks.length === 0 && todosToDisplay.length > 0 && !isAllCompleted;
}, [inProgressTasks.length, todosToDisplay.length, isAllCompleted]);
const [automaticExpanded, setAutomaticExpanded] = useState(desiredAutomaticExpanded);

// Keep the currently rendered automatic state for one layout commit. This
// lets the shared height contract publish the collapse intent before the
// second synchronous commit removes the expanded body.
useLayoutEffect(() => {
if (expandedState !== null || automaticExpanded === desiredAutomaticExpanded) {
return;
}
applyExpandedState(
automaticExpanded,
desiredAutomaticExpanded,
setAutomaticExpanded,
);
}, [
applyExpandedState,
automaticExpanded,
desiredAutomaticExpanded,
expandedState,
]);

const isExpanded = expandedState ?? automaticExpanded;

const isLoading = status === 'preparing' || status === 'streaming' || status === 'running';

const displayMode = config?.displayMode || 'compact';
Expand All @@ -110,9 +84,7 @@ export const TodoWriteDisplay: React.FC<ToolCardProps> = ({

const handleToggleExpanded = useCallback(() => {
if (todosToDisplay.length === 0) return;
applyExpandedState(isExpanded, !isExpanded, (nextExpanded) => {
setExpandedState(nextExpanded);
});
applyExpandedState(isExpanded, !isExpanded, setIsExpanded);
}, [applyExpandedState, isExpanded, todosToDisplay.length]);

const renderTodoItem = (todo: TodoLike, key: string) => (
Expand Down Expand Up @@ -246,6 +218,7 @@ export const TodoWriteDisplay: React.FC<ToolCardProps> = ({
isExpanded={isExpanded && hasTodos}
onClick={hasTodos ? handleToggleExpanded : undefined}
clickable={hasTodos}
toggleTestId="todo-write-toggle"
className="todo-write-card"
header={
<CompactToolCardHeader
Expand Down
Loading