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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
16439d343a
commit
c9bcf79ca7
@@ -617,6 +617,12 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
});
|
});
|
||||||
}, [toolParts]);
|
}, [toolParts]);
|
||||||
|
|
||||||
|
const isActiveTool = React.useCallback((toolPart: ToolPartType): boolean => {
|
||||||
|
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
|
||||||
|
const status = state?.status;
|
||||||
|
return status === 'pending' || status === 'running' || status === 'started';
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isToolFinalized = React.useCallback((toolPart: ToolPartType) => {
|
const isToolFinalized = React.useCallback((toolPart: ToolPartType) => {
|
||||||
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
|
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
|
||||||
const status = state?.status;
|
const status = state?.status;
|
||||||
@@ -635,6 +641,10 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
return true;
|
return true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const shouldShowTool = React.useCallback((toolPart: ToolPartType): boolean => {
|
||||||
|
return isActiveTool(toolPart) || isToolFinalized(toolPart);
|
||||||
|
}, [isActiveTool, isToolFinalized]);
|
||||||
|
|
||||||
const allToolsFinalized = React.useMemo(() => {
|
const allToolsFinalized = React.useMemo(() => {
|
||||||
if (toolParts.length === 0) {
|
if (toolParts.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -881,10 +891,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
if (isActivityStandaloneTool(toolPart.tool)) {
|
if (isActivityStandaloneTool(toolPart.tool)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (shouldHoldTools) {
|
return shouldShowTool(toolPart);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isToolFinalized(toolPart);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
displayableTools.forEach((toolPart, index) => {
|
displayableTools.forEach((toolPart, index) => {
|
||||||
@@ -895,7 +902,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return connections;
|
return connections;
|
||||||
}, [toolParts, shouldHoldTools, isToolFinalized]);
|
}, [toolParts, shouldShowTool]);
|
||||||
|
|
||||||
const activityPartsForTurn = React.useMemo(() => {
|
const activityPartsForTurn = React.useMemo(() => {
|
||||||
return turnGroupingContext?.activityParts ?? [];
|
return turnGroupingContext?.activityParts ?? [];
|
||||||
@@ -1063,22 +1070,21 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
let endTime: number | null = null;
|
let endTime: number | null = null;
|
||||||
let element: React.ReactNode | null = null;
|
let element: React.ReactNode | null = null;
|
||||||
|
|
||||||
if (!shouldShowActivityGroup) {
|
if (!shouldShowActivityGroup) {
|
||||||
if (activity.kind === 'tool') {
|
if (activity.kind === 'tool') {
|
||||||
const toolPart = part as ToolPartType;
|
const toolPart = part as ToolPartType;
|
||||||
|
|
||||||
if (isActivityStandaloneTool(toolPart.tool)) {
|
if (isActivityStandaloneTool(toolPart.tool)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolState = (toolPart as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state;
|
const toolState = (toolPart as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state;
|
||||||
const time = toolState?.time;
|
const time = toolState?.time;
|
||||||
const isFinalized = isToolFinalized(toolPart);
|
const isFinalized = isToolFinalized(toolPart);
|
||||||
const shouldShowTool = !shouldHoldTools && isFinalized;
|
|
||||||
|
|
||||||
if (!shouldShowTool) {
|
if (!shouldShowTool(toolPart)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = toolConnections[toolPart.id];
|
const connection = toolConnections[toolPart.id];
|
||||||
|
|
||||||
@@ -1158,7 +1164,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
onContentChange,
|
onContentChange,
|
||||||
onShowPopup,
|
onShowPopup,
|
||||||
onToggleTool,
|
onToggleTool,
|
||||||
shouldHoldTools,
|
shouldShowTool,
|
||||||
shouldShowActivityGroup,
|
shouldShowActivityGroup,
|
||||||
showReasoningTraces,
|
showReasoningTraces,
|
||||||
syntaxTheme,
|
syntaxTheme,
|
||||||
|
|||||||
@@ -107,9 +107,36 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
|
|
||||||
const toolConnections = getToolConnections(displayParts);
|
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(() => {
|
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]);
|
}, [displayParts]);
|
||||||
|
|
||||||
// Set of part IDs that were visible in collapsed state
|
// 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 isTaskTool = part.tool.toLowerCase() === 'task';
|
||||||
|
|
||||||
const isFinalized = state.status === 'completed' || state.status === 'error';
|
const status = state.status as string | undefined;
|
||||||
const isActive = state.status === 'running' || state.status === 'pending';
|
const isFinalized = status === 'completed' || status === 'error';
|
||||||
|
const isActive = status === 'running' || status === 'pending' || status === 'started';
|
||||||
const isError = state.status === 'error';
|
const isError = state.status === 'error';
|
||||||
|
|
||||||
|
|
||||||
@@ -1454,18 +1455,14 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
const input = stateWithData.input;
|
const input = stateWithData.input;
|
||||||
const time = stateWithData.time;
|
const time = stateWithData.time;
|
||||||
|
|
||||||
const [pinnedTaskTime, setPinnedTaskTime] = React.useState<{ start?: number; end?: number }>({});
|
const [pinnedTime, setPinnedTime] = React.useState<{ start?: number; end?: number }>({});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setPinnedTaskTime({});
|
setPinnedTime({});
|
||||||
}, [part.id]);
|
}, [part.id]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isTaskTool) {
|
setPinnedTime((prev) => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPinnedTaskTime((prev) => {
|
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
@@ -1481,10 +1478,10 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
|
|
||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
}, [isTaskTool, time?.end, time?.start]);
|
}, [time?.end, time?.start]);
|
||||||
|
|
||||||
const effectiveTimeStart = isTaskTool ? (pinnedTaskTime.start ?? time?.start) : time?.start;
|
const effectiveTimeStart = pinnedTime.start ?? time?.start;
|
||||||
const effectiveTimeEnd = isTaskTool ? (pinnedTaskTime.end ?? time?.end) : time?.end;
|
const effectiveTimeEnd = pinnedTime.end ?? time?.end;
|
||||||
|
|
||||||
const endedTimestampText = React.useMemo(() => {
|
const endedTimestampText = React.useMemo(() => {
|
||||||
if (typeof effectiveTimeEnd !== 'number' || !Number.isFinite(effectiveTimeEnd)) {
|
if (typeof effectiveTimeEnd !== 'number' || !Number.isFinite(effectiveTimeEnd)) {
|
||||||
@@ -1670,7 +1667,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
handleMainClick(event);
|
handleMainClick(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isFinalized && !isTaskTool) {
|
if (!isFinalized && !isActive && !isTaskTool) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1757,7 +1754,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
<LiveDuration
|
<LiveDuration
|
||||||
start={effectiveTimeStart}
|
start={effectiveTimeStart}
|
||||||
end={typeof effectiveTimeEnd === 'number' ? effectiveTimeEnd : undefined}
|
end={typeof effectiveTimeEnd === 'number' ? effectiveTimeEnd : undefined}
|
||||||
active={Boolean(isTaskTool && isActive && typeof effectiveTimeEnd !== 'number')}
|
active={Boolean(isActive && typeof effectiveTimeEnd !== 'number')}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{!isMobile && endedTimestampText ? (
|
{!isMobile && endedTimestampText ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user