fix(chat): show in-flight tools immediately and keep collapsed activity live (#563)

* fix(chat): render active tools before completion

* fix(chat): preserve running activity in collapsed view

* fix(chat): live-update duration for active tools

* fix: show live tool activity without timer resets

Show active tool rows before completion
Keep live duration stable during streaming updates

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Nelson Pires
2026-03-03 04:00:46 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 16439d343a
commit c9bcf79ca7
3 changed files with 65 additions and 35 deletions
@@ -107,9 +107,36 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
const toolConnections = getToolConnections(displayParts);
// For collapsed state: show last N items
// For collapsed state: show last N items, but ensure at least one in-flight item is visible if exists
const visibleCollapsedParts = React.useMemo(() => {
return displayParts.slice(-MAX_VISIBLE_COLLAPSED);
const defaultVisible = displayParts.slice(-MAX_VISIBLE_COLLAPSED);
const hasVisibleActive = defaultVisible.some((p) => p.endedAt === undefined);
if (hasVisibleActive) {
return defaultVisible;
}
const activeParts = displayParts.filter((p) => p.endedAt === undefined);
if (activeParts.length === 0) {
return defaultVisible;
}
const newestActive = activeParts[activeParts.length - 1];
const visibleIds = new Set(defaultVisible.map((p) => p.id));
if (visibleIds.has(newestActive.id)) {
return defaultVisible;
}
const replacementIndex = 0;
const result = [...defaultVisible];
result[replacementIndex] = newestActive;
return result.sort((a, b) => {
const aIndex = displayParts.findIndex((p) => p.id === a.id);
const bIndex = displayParts.findIndex((p) => p.id === b.id);
return aIndex - bIndex;
});
}, [displayParts]);
// Set of part IDs that were visible in collapsed state
@@ -1432,8 +1432,9 @@ const ToolPart: React.FC<ToolPartProps> = ({
const isTaskTool = part.tool.toLowerCase() === 'task';
const isFinalized = state.status === 'completed' || state.status === 'error';
const isActive = state.status === 'running' || state.status === 'pending';
const status = state.status as string | undefined;
const isFinalized = status === 'completed' || status === 'error';
const isActive = status === 'running' || status === 'pending' || status === 'started';
const isError = state.status === 'error';
@@ -1454,18 +1455,14 @@ const ToolPart: React.FC<ToolPartProps> = ({
const input = stateWithData.input;
const time = stateWithData.time;
const [pinnedTaskTime, setPinnedTaskTime] = React.useState<{ start?: number; end?: number }>({});
const [pinnedTime, setPinnedTime] = React.useState<{ start?: number; end?: number }>({});
React.useEffect(() => {
setPinnedTaskTime({});
setPinnedTime({});
}, [part.id]);
React.useEffect(() => {
if (!isTaskTool) {
return;
}
setPinnedTaskTime((prev) => {
setPinnedTime((prev) => {
const next = { ...prev };
let changed = false;
@@ -1481,10 +1478,10 @@ const ToolPart: React.FC<ToolPartProps> = ({
return changed ? next : prev;
});
}, [isTaskTool, time?.end, time?.start]);
}, [time?.end, time?.start]);
const effectiveTimeStart = isTaskTool ? (pinnedTaskTime.start ?? time?.start) : time?.start;
const effectiveTimeEnd = isTaskTool ? (pinnedTaskTime.end ?? time?.end) : time?.end;
const effectiveTimeStart = pinnedTime.start ?? time?.start;
const effectiveTimeEnd = pinnedTime.end ?? time?.end;
const endedTimestampText = React.useMemo(() => {
if (typeof effectiveTimeEnd !== 'number' || !Number.isFinite(effectiveTimeEnd)) {
@@ -1670,7 +1667,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
handleMainClick(event);
};
if (!isFinalized && !isTaskTool) {
if (!isFinalized && !isActive && !isTaskTool) {
return null;
}
@@ -1757,7 +1754,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
<LiveDuration
start={effectiveTimeStart}
end={typeof effectiveTimeEnd === 'number' ? effectiveTimeEnd : undefined}
active={Boolean(isTaskTool && isActive && typeof effectiveTimeEnd !== 'number')}
active={Boolean(isActive && typeof effectiveTimeEnd !== 'number')}
/>
</span>
{!isMobile && endedTimestampText ? (