From 9494051591200097cc82dbe08ad7c3b0a22f3e6f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 28 Apr 2026 22:05:39 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20Don't=20auto-open=20artif?= =?UTF-8?q?act=20panel=20on=20history=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigating to a previous conversation full of code-execution artifacts would auto-open the side panel and focus the most-recent artifact — the same code path that fires for fresh streaming artifacts. Users expect that "auto-open" behavior only when an artifact arrives via SSE, not when they revisit an old chat. Two-part gate: 1. `ToolArtifactCard`'s focus effect captures `isSubmitting` at first render via a ref. A card mounted *during* a stream means a new artifact arrived → steal panel focus (legacy behavior). A card mounted while `isSubmitting === false` is part of conversation history → leave focus alone. 2. `Presentation`'s panel-render condition gains `currentArtifactId != null`. With (1) keeping `currentArtifactId` null on history load, the panel stops rendering at all on navigation — even if `artifactsVisibility` was left `true` by a prior conversation. User clicks on a chip to re-open (the click handler is unchanged and unconditional). Test seeds `isSubmittingFamily(0)` per case: existing tests opt into streaming (default `true`) so legacy auto-focus assertions still hold; new tests for history-load opt into `streaming: false` and verify no auto-focus + click-to-open still works. --- .../Content/Parts/ToolArtifactCard.tsx | 39 ++++++++++++++----- client/src/components/Chat/Presentation.tsx | 16 +++++++- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Parts/ToolArtifactCard.tsx b/client/src/components/Chat/Messages/Content/Parts/ToolArtifactCard.tsx index cae6d197ff..92eec47303 100644 --- a/client/src/components/Chat/Messages/Content/Parts/ToolArtifactCard.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/ToolArtifactCard.tsx @@ -1,9 +1,10 @@ -import { memo, useEffect, useId, useLayoutEffect } from 'react'; +import { memo, useEffect, useId, useLayoutEffect, useRef } from 'react'; import { Download } from 'lucide-react'; import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'; import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider'; import type { Artifact } from '~/common'; import FilePreview from '~/components/Chat/Input/Files/FilePreview'; +import { displayFilename } from './attachmentTypes'; import { useAttachmentLink } from './LogLink'; import { useLocalize } from '~/hooks'; import { cn, getFileType } from '~/utils'; @@ -41,11 +42,15 @@ interface ToolArtifactCardProps { * Without that guard, both cards would observe each other's write * and trade overwrites in a loop. * - * 3. **Focus on mount** (deps: artifact.id). A freshly-mounted card - * means a new artifact has arrived; we steal panel focus to match - * the legacy streaming-artifact UX where the latest artifact - * auto-opens. Cards that re-render with the same artifact don't - * refire this, so user clicks on older cards aren't overridden. + * 3. **Focus on mount** (deps: artifact.id) — gated on `isSubmitting` + * captured at first render via a ref. A card mounted *during* + * streaming means a new artifact arrived from SSE; we steal panel + * focus to match the legacy streaming-artifact UX. A card mounted + * while `isSubmitting === false` is part of conversation history + * (page load, navigating back to an old conversation) and must + * not steal focus — `Presentation`'s render condition gates the + * panel on `currentArtifactId != null`, so leaving focus alone + * keeps the panel closed on history load. * * Visibility is intentionally not toggled. The Recoil default is `true`, * which auto-opens the panel on first registration; a user who has @@ -60,9 +65,15 @@ const ToolArtifactCard = memo(({ attachment, artifact }: ToolArtifactCardProps) const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId); const currentArtifactId = useRecoilValue(store.currentArtifactId); const existingEntry = useRecoilValue(store.artifactByIdSelector(artifact.id)); + const isSubmitting = useRecoilValue(store.isSubmittingFamily(0)); const [claim, setClaim] = useRecoilState(store.toolArtifactClaim(artifact.id)); const isSelected = artifact.id === currentArtifactId; const isMyClaim = claim === claimKey; + // Captured at first render only — cards that mount mid-stream stay + // "fresh" for the rest of their lifetime even after streaming ends, + // and cards that mount post-stream stay "history" even if the user + // sends another message while the same card stays mounted. + const mountedDuringStreamRef = useRef(isSubmitting); useLayoutEffect(() => { // Always (re)claim on mount — a later card for the same id displaces @@ -96,6 +107,11 @@ const ToolArtifactCard = memo(({ attachment, artifact }: ToolArtifactCardProps) }, [artifact, existingEntry, isMyClaim, setArtifacts]); useEffect(() => { + if (!mountedDuringStreamRef.current) { + // Card mounted as part of conversation history — leave focus alone + // so the side panel doesn't auto-open on navigation. + return; + } setCurrentArtifactId(artifact.id); }, [artifact.id, setCurrentArtifactId]); @@ -130,6 +146,11 @@ const ToolArtifactCard = memo(({ attachment, artifact }: ToolArtifactCardProps) const actionLabel = isSelected ? localize('com_ui_click_to_close') : localize('com_ui_artifact_click'); + const visibleFilename = displayFilename(attachment.filename); + // The artifact's stored `title` mirrors the on-disk `filename` for + // tool artifacts, so re-derive the user-facing label rather than + // showing the collision-suffixed name. + const visibleTitle = displayFilename(artifact.title); return (
@@ -149,8 +170,8 @@ const ToolArtifactCard = memo(({ attachment, artifact }: ToolArtifactCardProps)
-
- {artifact.title} +
+ {visibleTitle}
{actionLabel}
@@ -160,7 +181,7 @@ const ToolArtifactCard = memo(({ attachment, artifact }: ToolArtifactCardProps)