mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site/src/pages/AgentsPage): remove redundant memo and Context.Provider (#23507)
The React Compiler (babel-plugin-react-compiler@1.0.0) handles
memoization automatically for all components in the AgentsPage
compiled path. Three memo() wrappers were redundant:
- ChatMessageItem in ConversationTimeline.tsx
- LazyFileDiff in DiffViewer.tsx
- ChatTreeNode in AgentsSidebar.tsx
Also migrate three Context.Provider usages to the React 19
shorthand (<Context value={...}>) and simplify the EmbedContext
export to use the context directly instead of re-exporting
.Provider as an alias.
This commit is contained in:
@@ -11,7 +11,7 @@ import { Outlet, useParams } from "react-router";
|
||||
import type { AgentsOutletContext } from "./AgentsPage";
|
||||
import {
|
||||
bootstrapChatEmbedSession,
|
||||
EmbedProvider,
|
||||
EmbedContext,
|
||||
} from "./components/EmbedContext";
|
||||
import type { ChatDetailError } from "./utils/usageLimitMessage";
|
||||
|
||||
@@ -178,13 +178,13 @@ const AgentEmbedPage: FC = () => {
|
||||
|
||||
if (auth.isSignedIn) {
|
||||
return (
|
||||
<EmbedProvider value={{ isEmbedded: true }}>
|
||||
<EmbedContext value={{ isEmbedded: true }}>
|
||||
<DashboardProvider>
|
||||
<ProxyProvider>
|
||||
<Outlet context={outletContext} />
|
||||
</ProxyProvider>
|
||||
</DashboardProvider>
|
||||
</EmbedProvider>
|
||||
</EmbedContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import { FileTextIcon, PencilIcon } from "lucide-react";
|
||||
import {
|
||||
type FC,
|
||||
Fragment,
|
||||
memo,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
@@ -371,7 +370,7 @@ function renderBlockList({
|
||||
return { elements, renderedToolIDs };
|
||||
}
|
||||
|
||||
const ChatMessageItem = memo<{
|
||||
interface ChatMessageItemProps {
|
||||
message: TypesGen.ChatMessage;
|
||||
parsed: ParsedMessageContent;
|
||||
onEditUserMessage?: (
|
||||
@@ -387,250 +386,246 @@ const ChatMessageItem = memo<{
|
||||
// overlay to indicate truncated content.
|
||||
fadeFromBottom?: boolean;
|
||||
urlTransform?: UrlTransform;
|
||||
}>(
|
||||
({
|
||||
message,
|
||||
parsed,
|
||||
onEditUserMessage,
|
||||
editingMessageId,
|
||||
savingMessageId,
|
||||
isAfterEditingMessage = false,
|
||||
fadeFromBottom = false,
|
||||
}
|
||||
|
||||
const ChatMessageItem: FC<ChatMessageItemProps> = ({
|
||||
message,
|
||||
parsed,
|
||||
onEditUserMessage,
|
||||
editingMessageId,
|
||||
savingMessageId,
|
||||
isAfterEditingMessage = false,
|
||||
fadeFromBottom = false,
|
||||
urlTransform,
|
||||
}) => {
|
||||
const isUser = message.role === "user";
|
||||
const isSavingMessage = savingMessageId === message.id;
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [previewText, setPreviewText] = useState<string | null>(null);
|
||||
const toolByID = new Map(parsed.tools.map((tool) => [tool.id, tool]));
|
||||
|
||||
if (
|
||||
parsed.toolResults.length > 0 &&
|
||||
parsed.toolCalls.length === 0 &&
|
||||
parsed.markdown === "" &&
|
||||
parsed.reasoning === ""
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide messages that consist entirely of provider-executed
|
||||
// tool results. The parser skips these parts, so the parsed
|
||||
// output is empty and would show a "no renderable content"
|
||||
// fallback.
|
||||
const parts = message.content ?? [];
|
||||
if (
|
||||
parts.length > 0 &&
|
||||
parts.every((p) => p.type === "tool-result" && p.provider_executed)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasRenderableContent =
|
||||
parsed.blocks.length > 0 ||
|
||||
parsed.tools.length > 0 ||
|
||||
parsed.sources.length > 0;
|
||||
// Pre-compute the inline content for user messages so we
|
||||
// avoid a filter + map inside the JSX return path.
|
||||
const userInlineContent = isUser
|
||||
? parsed.blocks.filter(
|
||||
(
|
||||
b,
|
||||
): b is
|
||||
| Extract<RenderBlock, { type: "response" }>
|
||||
| Extract<RenderBlock, { type: "file-reference" }> =>
|
||||
b.type === "response" || b.type === "file-reference",
|
||||
)
|
||||
: [];
|
||||
|
||||
const userFileBlocks = isUser
|
||||
? parsed.blocks.filter(
|
||||
(b): b is Extract<RenderBlock, { type: "file" }> => b.type === "file",
|
||||
)
|
||||
: [];
|
||||
|
||||
const hasUserMessageBody =
|
||||
userInlineContent.length > 0 || Boolean(parsed.markdown?.trim());
|
||||
const hasFileBlocks = userFileBlocks.length > 0;
|
||||
|
||||
const conversationItemProps: { role: "user" | "assistant" } = {
|
||||
role: isUser ? "user" : "assistant",
|
||||
};
|
||||
const { elements: orderedBlocks, renderedToolIDs } = renderBlockList({
|
||||
blocks: parsed.blocks,
|
||||
toolByID,
|
||||
keyPrefix: String(message.id),
|
||||
onImageClick: setPreviewImage,
|
||||
onTextFileClick: (content) => setPreviewText(content),
|
||||
urlTransform,
|
||||
}) => {
|
||||
const isUser = message.role === "user";
|
||||
const isSavingMessage = savingMessageId === message.id;
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [previewText, setPreviewText] = useState<string | null>(null);
|
||||
const toolByID = new Map(parsed.tools.map((tool) => [tool.id, tool]));
|
||||
});
|
||||
const remainingTools = parsed.tools.filter(
|
||||
(tool) => !renderedToolIDs.has(tool.id),
|
||||
);
|
||||
|
||||
if (
|
||||
parsed.toolResults.length > 0 &&
|
||||
parsed.toolCalls.length === 0 &&
|
||||
parsed.markdown === "" &&
|
||||
parsed.reasoning === ""
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide messages that consist entirely of provider-executed
|
||||
// tool results. The parser skips these parts, so the parsed
|
||||
// output is empty and would show a "no renderable content"
|
||||
// fallback.
|
||||
const parts = message.content ?? [];
|
||||
if (
|
||||
parts.length > 0 &&
|
||||
parts.every((p) => p.type === "tool-result" && p.provider_executed)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasRenderableContent =
|
||||
parsed.blocks.length > 0 ||
|
||||
parsed.tools.length > 0 ||
|
||||
parsed.sources.length > 0;
|
||||
// Pre-compute the inline content for user messages so we
|
||||
// avoid a filter + map inside the JSX return path.
|
||||
const userInlineContent = isUser
|
||||
? parsed.blocks.filter(
|
||||
(
|
||||
b,
|
||||
): b is
|
||||
| Extract<RenderBlock, { type: "response" }>
|
||||
| Extract<RenderBlock, { type: "file-reference" }> =>
|
||||
b.type === "response" || b.type === "file-reference",
|
||||
)
|
||||
: [];
|
||||
|
||||
const userFileBlocks = isUser
|
||||
? parsed.blocks.filter(
|
||||
(b): b is Extract<RenderBlock, { type: "file" }> => b.type === "file",
|
||||
)
|
||||
: [];
|
||||
|
||||
const hasUserMessageBody =
|
||||
userInlineContent.length > 0 || Boolean(parsed.markdown?.trim());
|
||||
const hasFileBlocks = userFileBlocks.length > 0;
|
||||
|
||||
const conversationItemProps: { role: "user" | "assistant" } = {
|
||||
role: isUser ? "user" : "assistant",
|
||||
};
|
||||
const { elements: orderedBlocks, renderedToolIDs } = renderBlockList({
|
||||
blocks: parsed.blocks,
|
||||
toolByID,
|
||||
keyPrefix: String(message.id),
|
||||
onImageClick: setPreviewImage,
|
||||
onTextFileClick: (content) => setPreviewText(content),
|
||||
urlTransform,
|
||||
});
|
||||
const remainingTools = parsed.tools.filter(
|
||||
(tool) => !renderedToolIDs.has(tool.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isAfterEditingMessage && "opacity-40 pointer-events-none",
|
||||
"transition-opacity duration-200",
|
||||
)}
|
||||
>
|
||||
<ConversationItem {...conversationItemProps}>
|
||||
{isUser ? (
|
||||
<Message className="w-full max-w-none">
|
||||
<MessageContent
|
||||
className={cn(
|
||||
"group/msg rounded-lg border border-solid border-border-default bg-surface-secondary px-3 py-2 font-sans shadow-sm transition-shadow",
|
||||
editingMessageId === message.id &&
|
||||
"border-surface-secondary shadow-[0_0_0_2px_hsla(var(--border-warning),0.6)]",
|
||||
isSavingMessage && "ring-2 ring-content-secondary/40",
|
||||
fadeFromBottom && "relative overflow-hidden",
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isAfterEditingMessage && "opacity-40 pointer-events-none",
|
||||
"transition-opacity duration-200",
|
||||
)}
|
||||
>
|
||||
<ConversationItem {...conversationItemProps}>
|
||||
{isUser ? (
|
||||
<Message className="w-full max-w-none">
|
||||
<MessageContent
|
||||
className={cn(
|
||||
"group/msg rounded-lg border border-solid border-border-default bg-surface-secondary px-3 py-2 font-sans shadow-sm transition-shadow",
|
||||
editingMessageId === message.id &&
|
||||
"border-surface-secondary shadow-[0_0_0_2px_hsla(var(--border-warning),0.6)]",
|
||||
isSavingMessage && "ring-2 ring-content-secondary/40",
|
||||
fadeFromBottom && "relative overflow-hidden",
|
||||
)}
|
||||
style={
|
||||
fadeFromBottom
|
||||
? { maxHeight: "var(--clip-h, none)" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{(hasUserMessageBody || hasFileBlocks) && (
|
||||
<div className="flex items-start gap-2">
|
||||
{hasUserMessageBody && (
|
||||
<span className="min-w-0 flex-1">
|
||||
{userInlineContent.length > 0
|
||||
? userInlineContent.map((block, i) =>
|
||||
block.type === "response" ? (
|
||||
<Fragment key={i}>{block.text}</Fragment>
|
||||
) : (
|
||||
<FileReferenceChip
|
||||
key={i}
|
||||
fileName={block.file_name}
|
||||
startLine={block.start_line}
|
||||
endLine={block.end_line}
|
||||
className="mx-1"
|
||||
/>
|
||||
),
|
||||
)
|
||||
: parsed.markdown || ""}
|
||||
</span>
|
||||
)}
|
||||
{isSavingMessage && (
|
||||
<Spinner
|
||||
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-content-secondary"
|
||||
aria-label="Saving message edit"
|
||||
loading
|
||||
/>
|
||||
)}
|
||||
{onEditUserMessage && !isSavingMessage && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-content-secondary opacity-0 transition-opacity hover:bg-surface-tertiary hover:text-content-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link group-hover/msg:opacity-100"
|
||||
aria-label="Edit message"
|
||||
onClick={() => {
|
||||
const fileBlocks = parsed.blocks.filter(
|
||||
(
|
||||
b,
|
||||
): b is Extract<
|
||||
RenderBlock,
|
||||
{ type: "file" }
|
||||
> =>
|
||||
b.type === "file" &&
|
||||
(b.media_type.startsWith("image/") ||
|
||||
b.media_type === "text/plain"),
|
||||
);
|
||||
onEditUserMessage(
|
||||
message.id,
|
||||
parsed.markdown || "",
|
||||
fileBlocks.length > 0 ? fileBlocks : undefined,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Edit message</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
style={
|
||||
fadeFromBottom
|
||||
? { maxHeight: "var(--clip-h, none)" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{(hasUserMessageBody || hasFileBlocks) && (
|
||||
<div className="flex items-start gap-2">
|
||||
{hasUserMessageBody && (
|
||||
<span className="min-w-0 flex-1">
|
||||
{userInlineContent.length > 0
|
||||
? userInlineContent.map((block, i) =>
|
||||
block.type === "response" ? (
|
||||
<Fragment key={i}>{block.text}</Fragment>
|
||||
) : (
|
||||
<FileReferenceChip
|
||||
key={i}
|
||||
fileName={block.file_name}
|
||||
startLine={block.start_line}
|
||||
endLine={block.end_line}
|
||||
className="mx-1"
|
||||
/>
|
||||
),
|
||||
)
|
||||
: parsed.markdown || ""}
|
||||
</span>
|
||||
)}
|
||||
{isSavingMessage && (
|
||||
<Spinner
|
||||
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-content-secondary"
|
||||
aria-label="Saving message edit"
|
||||
loading
|
||||
/>
|
||||
)}
|
||||
{onEditUserMessage && !isSavingMessage && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-content-secondary opacity-0 transition-opacity hover:bg-surface-tertiary hover:text-content-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link group-hover/msg:opacity-100"
|
||||
aria-label="Edit message"
|
||||
onClick={() => {
|
||||
const fileBlocks = parsed.blocks.filter(
|
||||
(
|
||||
b,
|
||||
): b is Extract<
|
||||
RenderBlock,
|
||||
{ type: "file" }
|
||||
> =>
|
||||
b.type === "file" &&
|
||||
(b.media_type.startsWith("image/") ||
|
||||
b.media_type === "text/plain"),
|
||||
);
|
||||
onEditUserMessage(
|
||||
message.id,
|
||||
parsed.markdown || "",
|
||||
fileBlocks.length > 0
|
||||
? fileBlocks
|
||||
: undefined,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Edit message
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(() => {
|
||||
if (userFileBlocks.length === 0) return null;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
hasUserMessageBody && "mt-2",
|
||||
"flex flex-wrap gap-2",
|
||||
)}
|
||||
>
|
||||
{userFileBlocks.map((block, i) =>
|
||||
renderFileBlock({
|
||||
block,
|
||||
key: `user-file-${block.file_id ?? i}`,
|
||||
onImageClick: setPreviewImage,
|
||||
onTextFileClick: setPreviewText,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{fadeFromBottom && (
|
||||
{(() => {
|
||||
if (userFileBlocks.length === 0) return null;
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 max-h-12"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, hsl(var(--surface-secondary)), transparent)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
) : (
|
||||
<Message className="w-full">
|
||||
<MessageContent className="whitespace-normal">
|
||||
<div className="space-y-3">
|
||||
{orderedBlocks}
|
||||
{remainingTools.map((tool) => (
|
||||
<Tool
|
||||
key={tool.id}
|
||||
name={tool.name}
|
||||
args={tool.args}
|
||||
result={tool.result}
|
||||
status={tool.status}
|
||||
isError={tool.isError}
|
||||
/>
|
||||
))}
|
||||
{!hasRenderableContent && (
|
||||
<div className="text-xs text-content-secondary">
|
||||
Message has no renderable content.
|
||||
className={cn(
|
||||
hasUserMessageBody && "mt-2",
|
||||
"flex flex-wrap gap-2",
|
||||
)}
|
||||
>
|
||||
{userFileBlocks.map((block, i) =>
|
||||
renderFileBlock({
|
||||
block,
|
||||
key: `user-file-${block.file_id ?? i}`,
|
||||
onImageClick: setPreviewImage,
|
||||
onTextFileClick: setPreviewText,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
)}
|
||||
</ConversationItem>
|
||||
{previewImage && (
|
||||
<ImageLightbox
|
||||
src={previewImage}
|
||||
onClose={() => setPreviewImage(null)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{fadeFromBottom && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 max-h-12"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, hsl(var(--surface-secondary)), transparent)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
) : (
|
||||
<Message className="w-full">
|
||||
<MessageContent className="whitespace-normal">
|
||||
<div className="space-y-3">
|
||||
{orderedBlocks}
|
||||
{remainingTools.map((tool) => (
|
||||
<Tool
|
||||
key={tool.id}
|
||||
name={tool.name}
|
||||
args={tool.args}
|
||||
result={tool.result}
|
||||
status={tool.status}
|
||||
isError={tool.isError}
|
||||
/>
|
||||
))}
|
||||
{!hasRenderableContent && (
|
||||
<div className="text-xs text-content-secondary">
|
||||
Message has no renderable content.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
)}
|
||||
{previewText !== null && (
|
||||
<TextPreviewDialog
|
||||
content={previewText}
|
||||
onClose={() => setPreviewText(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</ConversationItem>
|
||||
{previewImage && (
|
||||
<ImageLightbox
|
||||
src={previewImage}
|
||||
onClose={() => setPreviewImage(null)}
|
||||
/>
|
||||
)}
|
||||
{previewText !== null && (
|
||||
<TextPreviewDialog
|
||||
content={previewText}
|
||||
onClose={() => setPreviewText(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StreamingOutput: FC<{
|
||||
streamState: StreamState | null;
|
||||
|
||||
@@ -19,7 +19,6 @@ import { ChevronRightIcon } from "lucide-react";
|
||||
import {
|
||||
type ComponentProps,
|
||||
type FC,
|
||||
memo,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -408,11 +407,11 @@ const DiffScrollContainer: FC<{
|
||||
scrollBarClassName="w-1.5"
|
||||
viewportClassName="[&>div]:!block"
|
||||
>
|
||||
<VirtualizerContext.Provider value={virtualizer}>
|
||||
<VirtualizerContext value={virtualizer}>
|
||||
<div ref={contentRef} className="min-w-0 text-xs">
|
||||
{children}
|
||||
</div>
|
||||
</VirtualizerContext.Provider>
|
||||
</VirtualizerContext>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -430,72 +429,72 @@ const DiffScrollContainer: FC<{
|
||||
* FileDiff that the user has already scrolled past, which avoids
|
||||
* layout shifts and repeated highlighting work.
|
||||
*/
|
||||
const LazyFileDiff = memo<{
|
||||
interface LazyFileDiffProps {
|
||||
fileDiff: FileDiffMetadata;
|
||||
options: ComponentProps<typeof FileDiff>["options"];
|
||||
lineAnnotations?: DiffLineAnnotation<string>[];
|
||||
renderAnnotation?: (annotation: DiffLineAnnotation<string>) => ReactNode;
|
||||
selectedLines?: SelectedLineRange | null;
|
||||
}>(
|
||||
({
|
||||
fileDiff,
|
||||
options,
|
||||
lineAnnotations,
|
||||
renderAnnotation: renderAnnotationProp,
|
||||
selectedLines,
|
||||
}) => {
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const el = placeholderRef.current;
|
||||
if (!el || visible) {
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
// Pre-load files that are within one viewport-height of
|
||||
// the visible area so they are ready before the user
|
||||
// scrolls to them.
|
||||
{ rootMargin: "100% 0px" },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [visible]);
|
||||
const LazyFileDiff: FC<LazyFileDiffProps> = ({
|
||||
fileDiff,
|
||||
options,
|
||||
lineAnnotations,
|
||||
renderAnnotation: renderAnnotationProp,
|
||||
selectedLines,
|
||||
}) => {
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
if (!visible) {
|
||||
return (
|
||||
<div
|
||||
ref={placeholderRef}
|
||||
style={{ height: estimateDiffHeight(fileDiff) }}
|
||||
className="p-4 space-y-2"
|
||||
>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
const el = placeholderRef.current;
|
||||
if (!el || visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<FileDiff
|
||||
fileDiff={fileDiff}
|
||||
options={options}
|
||||
metrics={VIRTUALIZER_METRICS}
|
||||
style={DIFFS_FONT_STYLE}
|
||||
lineAnnotations={lineAnnotations}
|
||||
renderAnnotation={renderAnnotationProp}
|
||||
selectedLines={selectedLines}
|
||||
/>
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
// Pre-load files that are within one viewport-height of
|
||||
// the visible area so they are ready before the user
|
||||
// scrolls to them.
|
||||
{ rootMargin: "100% 0px" },
|
||||
);
|
||||
},
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) {
|
||||
return (
|
||||
<div
|
||||
ref={placeholderRef}
|
||||
style={{ height: estimateDiffHeight(fileDiff) }}
|
||||
className="p-4 space-y-2"
|
||||
>
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileDiff
|
||||
fileDiff={fileDiff}
|
||||
options={options}
|
||||
metrics={VIRTUALIZER_METRICS}
|
||||
style={DIFFS_FONT_STYLE}
|
||||
lineAnnotations={lineAnnotations}
|
||||
renderAnnotation={renderAnnotationProp}
|
||||
selectedLines={selectedLines}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Main component
|
||||
|
||||
@@ -13,7 +13,7 @@ const EmbedContext = createContext<EmbedContextValue>({
|
||||
isEmbedded: false,
|
||||
});
|
||||
|
||||
export const EmbedProvider = EmbedContext.Provider;
|
||||
export { EmbedContext };
|
||||
|
||||
export const useEmbedContext = () => useContext(EmbedContext);
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import {
|
||||
createContext,
|
||||
type FC,
|
||||
memo,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
@@ -354,7 +353,7 @@ interface ChatTreeNodeProps {
|
||||
readonly isChildNode: boolean;
|
||||
}
|
||||
|
||||
const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
|
||||
const {
|
||||
chatTree,
|
||||
chatById,
|
||||
@@ -578,7 +577,7 @@ const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
const {
|
||||
@@ -786,7 +785,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ChatTreeContext.Provider value={chatTreeCtx}>
|
||||
<ChatTreeContext value={chatTreeCtx}>
|
||||
{visibleRootIDs.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border-default bg-surface-primary p-4 text-center text-xs text-content-secondary">
|
||||
<p className="m-0">
|
||||
@@ -889,7 +888,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</ChatTreeContext.Provider>
|
||||
</ChatTreeContext>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
Reference in New Issue
Block a user