{
);
expect(screen.queryByText('bounded input')).not.toBeInTheDocument();
- expect(screen.getByText('com_ui_subagent_activity_details_truncated')).toBeInTheDocument();
+ expect(
+ screen.queryByText('com_ui_subagent_activity_details_truncated'),
+ ).not.toBeInTheDocument();
expect(screen.queryByText('com_ui_subagent_thread_history_truncated')).not.toBeInTheDocument();
});
diff --git a/client/src/components/Chat/Subagents/SubagentActivity.tsx b/client/src/components/Chat/Subagents/SubagentActivity.tsx
index 23b2e955b2..da435f03fa 100644
--- a/client/src/components/Chat/Subagents/SubagentActivity.tsx
+++ b/client/src/components/Chat/Subagents/SubagentActivity.tsx
@@ -291,21 +291,11 @@ function SubagentPrompt({ prompt }: { prompt: string }) {
);
}
-export const hasTruncatedActivityDetails = (activity: ChildActivity): boolean =>
- activity.items.some(
- (item) =>
- (item.type === 'writing' && item.textTruncated === true) ||
- (item.type === 'reasoning' && item.textTruncated === true) ||
- (item.type === 'activity_label' && item.labelTruncated === true) ||
- (item.type === 'tool' && (item.inputTruncated === true || item.outputTruncated === true)),
- );
-
export function SubagentActivityContent({
activity,
activityId,
state = 'ready',
showPrompt = true,
- showDetailTruncationNotice = true,
conversationId = null,
onCancelControl,
}: {
@@ -313,14 +303,12 @@ export function SubagentActivityContent({
activityId?: string;
state?: 'ready' | 'loading' | 'error';
showPrompt?: boolean;
- showDetailTruncationNotice?: boolean;
conversationId?: string | null;
onCancelControl?: (controlId: string) => void;
}) {
const localize = useLocalize();
const isSubmitting = activity.status === 'running' || activity.status === 'dispatched';
const parts = useMemo(() => activity.items.map(toContentPart), [activity.items]);
- const activityDetailsTruncated = hasTruncatedActivityDetails(activity);
let body: React.ReactNode;
if (state === 'loading') {
@@ -367,11 +355,6 @@ export function SubagentActivityContent({
{localize('com_ui_subagent_control_history_truncated')}
)}
- {showDetailTruncationNotice && activityDetailsTruncated && (
-
- {localize('com_ui_subagent_activity_details_truncated')}
-
- )}
{body}
);
diff --git a/client/src/components/Chat/Subagents/SubagentConversation.test.tsx b/client/src/components/Chat/Subagents/SubagentConversation.test.tsx
index 8c20c728bb..3ff3a6990c 100644
--- a/client/src/components/Chat/Subagents/SubagentConversation.test.tsx
+++ b/client/src/components/Chat/Subagents/SubagentConversation.test.tsx
@@ -120,10 +120,13 @@ describe('SubagentConversation', () => {
expect(container.querySelectorAll('.agent-turn')).toHaveLength(2);
expect(container.querySelector('[data-subagent-conversation]')).toBeInTheDocument();
expect(screen.queryByText('com_ui_prompt')).not.toBeInTheDocument();
- expect(screen.getByText('com_ui_subagent_activity_details_truncated')).toBeInTheDocument();
+ expect(
+ screen.queryByText('com_ui_subagent_activity_details_truncated'),
+ ).not.toBeInTheDocument();
expect(
screen.queryByText('com_ui_subagent_activity_details_unavailable'),
).not.toBeInTheDocument();
+ expect(screen.getByTestId('stream-elapsed')).toBeInTheDocument();
fireEvent.click(
screen.getByRole('button', {
diff --git a/client/src/components/Chat/Subagents/SubagentConversation.tsx b/client/src/components/Chat/Subagents/SubagentConversation.tsx
index c6a7f8b4d0..29982d619e 100644
--- a/client/src/components/Chat/Subagents/SubagentConversation.tsx
+++ b/client/src/components/Chat/Subagents/SubagentConversation.tsx
@@ -7,13 +7,10 @@ import type { TMessageContentParts } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import type { ChildConversationTurn } from './adapters';
import type { TranslationKeys } from '~/hooks';
-import {
- hasTruncatedActivityDetails,
- SubagentActivityContent,
- SubagentStatus,
-} from './SubagentActivity';
+import { SubagentActivityContent, SubagentStatus } from './SubagentActivity';
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
import MessageRow from '~/components/Chat/Messages/ui/MessageRow';
+import { ElapsedTimer } from '~/components/Chat/Messages/Elapsed';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { isAbnormalTerminalStatus } from './status';
import { useAgentsMapContext } from '~/Providers';
@@ -191,10 +188,9 @@ function ChildMessage({
const agentsMap = useAgentsMapContext();
const agent = agentId == null ? undefined : agentsMap?.[agentId];
const label = agent?.name ?? turn.activity.title;
- const wholeActivityTruncated = turn.activity.activityTruncated === true;
- const detailsLimited = wholeActivityTruncated || hasTruncatedActivityDetails(turn.activity);
+ const detailsLimited = turn.activity.activityTruncated === true;
let limitedNotice: ReactNode;
- if (wholeActivityTruncated && onLoadDetails != null && detailState !== 'unavailable') {
+ if (detailsLimited && onLoadDetails != null && detailState !== 'unavailable') {
limitedNotice = (
);
- } else if (wholeActivityTruncated) {
- limitedNotice = localize('com_ui_subagent_activity_details_unavailable');
} else {
- /** Only item-level fields were shortened for display; the run's full
- * activity is otherwise present, so avoid the alarming "unavailable"
- * framing there. */
- limitedNotice = (
- {localize('com_ui_subagent_activity_details_truncated')}
- );
+ limitedNotice = localize('com_ui_subagent_activity_details_unavailable');
+ }
+ let footer: ReactNode = null;
+ if (isAbnormalTerminalStatus(turn.activity.status)) {
+ footer = ;
+ } else if (turn.activity.status === 'running' || turn.activity.status === 'dispatched') {
+ const triggeredAt = turn.trigger.createdAt ?? turn.trigger.externalEvent?.occurredAt;
+ const startedAt = triggeredAt == null ? NaN : Date.parse(triggeredAt);
+ footer = ;
}
const iconData = {
endpoint: EModelEndpoint.agents,
@@ -230,11 +227,7 @@ function ChildMessage({
)
}
label={label}
- footer={
- isAbnormalTerminalStatus(turn.activity.status) ? (
-
- ) : null
- }
+ footer={footer}
ariaLabel={label}
headerPrefix=""
isCreatedByUser={false}
@@ -245,7 +238,6 @@ function ChildMessage({
activityId={`${turn.taskId}:assistant`}
state={state}
showPrompt={false}
- showDetailTruncationNotice={false}
conversationId={conversationId}
onCancelControl={onCancelControl}
/>
diff --git a/client/src/components/Chat/Subagents/SubagentThreadPanel.test.tsx b/client/src/components/Chat/Subagents/SubagentThreadPanel.test.tsx
index 9a6d50efd7..51e1dfbf65 100644
--- a/client/src/components/Chat/Subagents/SubagentThreadPanel.test.tsx
+++ b/client/src/components/Chat/Subagents/SubagentThreadPanel.test.tsx
@@ -1483,6 +1483,67 @@ describe('SubagentThreadPanel', () => {
expect(screen.queryByText('com_ui_subagent_depth')).not.toBeInTheDocument();
});
+ it.each([
+ ['newest known task', 'task-new', 'last'],
+ ['displaced older task', 'task-old-displaced', 'first'],
+ ] as const)(
+ 'places a turn missing from the durable window per its identity: %s',
+ (_label, selectedTaskId, position) => {
+ mockParentChildrenByThread = new Map([
+ [
+ 'child-thread',
+ {
+ threadId: 'child-thread',
+ parentMessageId: 'parent-message',
+ parentToolCallId: 'tool-call',
+ subagentType: 'researcher',
+ subagentKind: 'agent',
+ title: 'Research child',
+ origin: 'tool',
+ status: 'running',
+ latestTaskId: 'task-new',
+ tasks: [{ taskId: 'task-new', status: 'running' }],
+ tasksTruncated: false,
+ } as ParentSubagentSummary,
+ ],
+ ]);
+ mockUseSubagentThreadQuery.mockReturnValue({
+ data: {
+ ...completedView,
+ turns: [
+ {
+ taskId: 'task-durable',
+ trigger: { kind: 'parent_dispatch', summary: 'Durable window turn' },
+ status: 'completed',
+ activity: [{ type: 'writing', text: 'Durable result' }],
+ activityTruncated: false,
+ messages: [],
+ },
+ ],
+ },
+ isLoading: false,
+ isError: false,
+ isReadinessPending: false,
+ });
+ const missingSelection: ActiveSubagentPanel = {
+ ...selection,
+ prompt: 'Synthesized window turn',
+ durable: { threadId: 'child-thread', taskId: selectedTaskId },
+ };
+ render(
+ set(activeSubagentPanel, missingSelection)}>
+
+ ,
+ );
+
+ const turns = screen.getAllByTestId('conversation-turn');
+ expect(turns).toHaveLength(2);
+ const synthesizedIndex = position === 'last' ? 1 : 0;
+ expect(turns[synthesizedIndex]).toHaveTextContent('Synthesized window turn');
+ expect(turns[1 - synthesizedIndex]).toHaveTextContent('Durable window turn');
+ },
+ );
+
it('loads an exact older turn projection only after the local disclosure is opened', async () => {
const truncatedView: SubagentThreadView = {
...completedView,
diff --git a/client/src/components/Chat/Subagents/SubagentThreadPanel.tsx b/client/src/components/Chat/Subagents/SubagentThreadPanel.tsx
index b39f5fa375..281ba5ca26 100644
--- a/client/src/components/Chat/Subagents/SubagentThreadPanel.tsx
+++ b/client/src/components/Chat/Subagents/SubagentThreadPanel.tsx
@@ -796,23 +796,38 @@ export default function SubagentThreadPanel({ selection }: { selection: ActiveSu
return override == null ? selected : { ...selected, activity: override };
});
}
- // The API keeps the exact selected activity even when its bounded
- // chronological turn is the first item removed from the response.
- // Preserve that selection ahead of the retained newer continuation.
- const retained = [
- {
- taskId: taskId || `${selection.parentMessageId}:${selection.toolCallId}`,
- trigger: {
- kind:
- selection.event == null
- ? ('parent_continuation' as const)
- : ('external_event' as const),
- summary: selection.prompt ?? activity.prompt ?? '',
- },
- activity,
+ /** A turn absent from the durable window is either the thread's newest
+ * delivery whose fetch has not landed yet (place it at the END, where
+ * it will settle — a new event must not appear at the top and then
+ * jump to the bottom) or an older selection displaced from the bounded
+ * window (keep it ahead of the retained newer continuation). */
+ const indexedChild = byThreadId.get(threadId);
+ const selectedTaskCreatedAt = indexedChild?.tasks.find(
+ (task) => task.taskId === taskId,
+ )?.createdAt;
+ const synthesizedTurn = {
+ taskId: taskId || `${selection.parentMessageId}:${selection.toolCallId}`,
+ trigger: {
+ kind:
+ selection.event == null
+ ? ('parent_continuation' as const)
+ : ('external_event' as const),
+ summary: selection.prompt ?? activity.prompt ?? '',
+ ...(selectedTaskCreatedAt == null ? {} : { createdAt: selectedTaskCreatedAt }),
},
- ...durableTurns,
- ];
+ activity,
+ };
+ /** Order by trigger time when both sides carry one; the separately
+ * polled discovery index is only the fallback authority, since it can
+ * briefly lag or lead the thread view. */
+ const lastDurableCreatedAt = durableTurns[durableTurns.length - 1]?.trigger.createdAt;
+ const appendSynthesized =
+ selectedTaskCreatedAt != null && lastDurableCreatedAt != null
+ ? selectedTaskCreatedAt >= lastDurableCreatedAt
+ : indexedChild?.latestTaskId === taskId;
+ const retained = appendSynthesized
+ ? [...durableTurns, synthesizedTurn]
+ : [synthesizedTurn, ...durableTurns];
return retained.map((turn) => {
const override = turnDetailOverrides.get(turn.taskId);
return override == null ? turn : { ...turn, activity: override };
@@ -831,12 +846,14 @@ export default function SubagentThreadPanel({ selection }: { selection: ActiveSu
];
}, [
activity,
+ byThreadId,
latestConversationTurns,
movingWindowTurns,
olderTurns,
postRebaseTurns,
rebaseTurns,
retainedTurnsValid,
+ threadId,
selection,
taskId,
turnDetailOverrides,
diff --git a/client/src/components/Chat/Subagents/status.ts b/client/src/components/Chat/Subagents/status.ts
index 51160caa42..951086bdd9 100644
--- a/client/src/components/Chat/Subagents/status.ts
+++ b/client/src/components/Chat/Subagents/status.ts
@@ -24,3 +24,13 @@ export const subagentStatusLabelKey = (status: SubagentThreadStatus) =>
cancelled: 'com_ui_subagent_thread_status_cancelled',
}) as const
)[status];
+
+/** Fixed-size status dot classes: a stable-width indicator that cannot make
+ * rows shift as status text changes length. */
+export const subagentStatusDotClass = (status: SubagentThreadStatus): string => {
+ if (status === 'completed') return 'bg-status-success';
+ if (status === 'running') return 'animate-pulse bg-status-info';
+ if (status === 'failed' || status === 'interrupted') return 'bg-status-error';
+ if (status === 'cancelled') return 'bg-status-warning';
+ return 'bg-text-tertiary';
+};
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 83c279259b..7e27e2ad98 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -2233,7 +2233,6 @@
"com_ui_subagent_control_cancel_message": "Withdraw message",
"com_ui_subagent_control_history": "Control history",
"com_ui_subagent_control_history_truncated": "Earlier control activity is not shown.",
- "com_ui_subagent_activity_details_truncated": "Some activity details were shortened.",
"com_ui_subagent_activity_details_unavailable": "Additional activity details are unavailable.",
"com_ui_subagent_show_full_activity": "Show full activity",
"com_ui_subagent_load_earlier_activity": "Load earlier activity",