fix(vscode): reduce webview streaming re-render churn (#1994)

Chat components subscribed to the entire chat store, so every
streaming delta re-rendered every assistant message. Subscribe to
narrow store slices and memoize ChatMessage so settled messages no
longer re-render on each delta. Cap getImageDataUri at 10MB, matching
the media picker, so oversized local images are not inlined into the
webview DOM.
This commit is contained in:
Kai 2026-07-21 13:10:59 +08:00 committed by GitHub
parent 6dd4fd3368
commit beeb964393
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 16 additions and 9 deletions

View file

@ -165,6 +165,10 @@ const getImageDataUri: Handler<FilePathParams, string | null> = async ({ filePat
const mime = IMAGE_MIME_TYPES[path.extname(resolved.relativePath).toLowerCase()];
if (!mime) return null;
try {
// Same cap as the media picker: an oversized image inlined as a data URI
// would live in the webview DOM (and its decoded bitmap) forever.
const stat = await vscode.workspace.fs.stat(resolved.uri);
if (stat.size > 10 * 1024 * 1024) return null;
const bytes = await vscode.workspace.fs.readFile(resolved.uri);
return `data:${mime};base64,${Buffer.from(bytes).toString("base64")}`;
} catch {

View file

@ -23,7 +23,8 @@ function ScrollButton() {
}
function MessageList() {
const { messages, isStreaming } = useChatStore();
const messages = useChatStore((s) => s.messages);
const isStreaming = useChatStore((s) => s.isStreaming);
return (
<>
@ -43,9 +44,9 @@ function MessageList() {
}
export function ChatArea() {
const { messages } = useChatStore();
const messageCount = useChatStore((s) => s.messages.length);
if (messages.length === 0) {
if (messageCount === 0) {
return (
<div className="h-full flex items-center justify-center relative">
<WelcomeScreen />

View file

@ -1,4 +1,4 @@
import { useState, Fragment } from "react";
import { useState, Fragment, memo } from "react";
import { IconLoader3, IconGitFork } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { Content } from "@/lib/content";
@ -146,7 +146,9 @@ interface ForkButtonProps {
function ForkButton({ turnIndex, className }: ForkButtonProps) {
const [showConfirm, setShowConfirm] = useState(false);
const [isForking, setIsForking] = useState(false);
const { sessionId, isStreaming, loadSession } = useChatStore();
const sessionId = useChatStore((s) => s.sessionId);
const isStreaming = useChatStore((s) => s.isStreaming);
const loadSession = useChatStore((s) => s.loadSession);
const handleFork = () => {
if (!sessionId || turnIndex < 0) return;
@ -234,7 +236,7 @@ function UserMessage({ message }: { message: ChatMessageType }) {
function AssistantMessage({ message, turnIndex, isStreaming }: { message: ChatMessageType; turnIndex?: number; isStreaming?: boolean }) {
const [previewMedia, setPreviewMedia] = useState<string | null>(null);
const { isCompacting } = useChatStore();
const isCompacting = useChatStore((s) => s.isCompacting);
const steps = message.steps || [];
const hasSteps = steps.length > 0;
@ -330,9 +332,9 @@ function hasMessageContent(message: ChatMessageType): boolean {
return message.steps?.some((s) => s.items.length > 0) ?? false;
}
export function ChatMessage({ message, turnIndex, isStreaming }: ChatMessageProps) {
export const ChatMessage = memo(function ChatMessage({ message, turnIndex, isStreaming }: ChatMessageProps) {
if (message.role === "user") {
return <UserMessage message={message} />;
}
return <AssistantMessage message={message} turnIndex={turnIndex} isStreaming={isStreaming} />;
}
});

View file

@ -2,7 +2,7 @@ import { IconLoader2 } from "@tabler/icons-react";
import { useChatStore } from "@/stores";
export function CompactionCard() {
const { isCompacting } = useChatStore();
const isCompacting = useChatStore((s) => s.isCompacting);
return (
<div className="rounded-lg border border-border bg-muted/20 overflow-hidden">