mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
fix(app): keep file image previews current
This commit is contained in:
@@ -694,6 +694,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
toast={toast}
|
||||
>
|
||||
<AssistantMessage
|
||||
occurrenceKey={item.id}
|
||||
message={item.text}
|
||||
timestamp={item.timestamp.getTime()}
|
||||
workspaceRoot={workspaceRoot}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createAssistantImageAcquisitionCache } from "./acquisition-cache";
|
||||
import {
|
||||
createAssistantImageAcquisitionCache,
|
||||
createAssistantImageFileAcquisitionKey,
|
||||
} from "./acquisition-cache";
|
||||
|
||||
describe("assistant image acquisition cache", () => {
|
||||
it("evicts a rejected acquisition so the next request can retry", async () => {
|
||||
@@ -43,4 +46,44 @@ describe("assistant image acquisition cache", () => {
|
||||
size: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses an acquired image when the current locator is unavailable", async () => {
|
||||
const cache = createAssistantImageAcquisitionCache<string>({ capacity: 2 });
|
||||
let unavailableCalls = 0;
|
||||
|
||||
await cache.acquire("message:image", async () => "persisted attachment");
|
||||
const cached = await cache.acquire("message:image", async () => {
|
||||
unavailableCalls += 1;
|
||||
throw new Error("daemon disconnected");
|
||||
});
|
||||
|
||||
expect({ cached, unavailableCalls }).toEqual({
|
||||
cached: "persisted attachment",
|
||||
unavailableCalls: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("scopes file acquisitions to the rendered message occurrence", () => {
|
||||
const first = createAssistantImageFileAcquisitionKey({
|
||||
serverId: "server",
|
||||
occurrenceKey: "message-1:image-1",
|
||||
cwd: "/workspace",
|
||||
path: "screenshot.png",
|
||||
});
|
||||
const remount = createAssistantImageFileAcquisitionKey({
|
||||
serverId: "server",
|
||||
occurrenceKey: "message-1:image-1",
|
||||
cwd: "/workspace",
|
||||
path: "screenshot.png",
|
||||
});
|
||||
const laterMessage = createAssistantImageFileAcquisitionKey({
|
||||
serverId: "server",
|
||||
occurrenceKey: "message-2:image-1",
|
||||
cwd: "/workspace",
|
||||
path: "screenshot.png",
|
||||
});
|
||||
|
||||
expect(remount).toBe(first);
|
||||
expect(laterMessage).not.toBe(first);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,15 @@ export interface AssistantImageAcquisitionCache<T> {
|
||||
size(): number;
|
||||
}
|
||||
|
||||
export function createAssistantImageFileAcquisitionKey(input: {
|
||||
serverId?: string;
|
||||
occurrenceKey: string;
|
||||
cwd: string;
|
||||
path: string;
|
||||
}): string {
|
||||
return `file:${input.serverId ?? "unknown-server"}:${input.occurrenceKey}:${input.cwd}:${input.path}`;
|
||||
}
|
||||
|
||||
export function createAssistantImageAcquisitionCache<T>(input: {
|
||||
capacity: number;
|
||||
}): AssistantImageAcquisitionCache<T> {
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
} from "@/utils/assistant-image-metadata";
|
||||
import { resolveAssistantImageSource } from "@/utils/assistant-image-source";
|
||||
import type { AssistantImageSourceResolution } from "@/utils/assistant-image-source";
|
||||
import { createAssistantImageAcquisitionCache } from "./acquisition-cache";
|
||||
import {
|
||||
createAssistantImageAcquisitionCache,
|
||||
createAssistantImageFileAcquisitionKey,
|
||||
} from "./acquisition-cache";
|
||||
import {
|
||||
createAssistantImageLifecycle,
|
||||
transitionAssistantImageLifecycle,
|
||||
@@ -49,6 +52,7 @@ export type AssistantImageResult =
|
||||
|
||||
interface UseAssistantImageInput {
|
||||
source: string;
|
||||
occurrenceKey: string;
|
||||
client?: DaemonClient | null;
|
||||
workspaceRoot?: string;
|
||||
serverId?: string;
|
||||
@@ -122,15 +126,24 @@ function createFileAcquisition(input: {
|
||||
client?: DaemonClient | null;
|
||||
resolution: AssistantImageSourceResolution | null;
|
||||
serverId?: string;
|
||||
occurrenceKey: string;
|
||||
unavailableMessage: string;
|
||||
}): AttachmentAcquisition | null {
|
||||
if (!input.client || input.resolution?.kind !== "file_rpc") {
|
||||
if (input.resolution?.kind !== "file_rpc") {
|
||||
return null;
|
||||
}
|
||||
const { client, resolution } = input;
|
||||
return {
|
||||
key: `file:${input.serverId ?? "unknown-server"}:${resolution.cwd}:${resolution.path}`,
|
||||
key: createAssistantImageFileAcquisitionKey({
|
||||
serverId: input.serverId,
|
||||
occurrenceKey: input.occurrenceKey,
|
||||
cwd: resolution.cwd,
|
||||
path: resolution.path,
|
||||
}),
|
||||
locate: async () => {
|
||||
if (!client) {
|
||||
throw new Error(input.unavailableMessage);
|
||||
}
|
||||
const file = await client.readFile(resolution.cwd, resolution.path);
|
||||
if (file.kind !== "image") {
|
||||
throw new Error(input.unavailableMessage);
|
||||
@@ -265,6 +278,7 @@ function getAcquisitionFailure(input: {
|
||||
|
||||
export function useAssistantImage({
|
||||
source,
|
||||
occurrenceKey,
|
||||
client,
|
||||
workspaceRoot,
|
||||
serverId,
|
||||
@@ -281,9 +295,10 @@ export function useAssistantImage({
|
||||
client,
|
||||
resolution,
|
||||
serverId,
|
||||
occurrenceKey,
|
||||
unavailableMessage: t("message.attachments.imagePreviewUnavailable"),
|
||||
}),
|
||||
[client, resolution, serverId, t],
|
||||
[client, occurrenceKey, resolution, serverId, t],
|
||||
);
|
||||
const dataImageAcquisition = useMemo(
|
||||
() => createDataImageAcquisition({ source, dataImage }),
|
||||
|
||||
@@ -723,6 +723,7 @@ export const LiveElapsed = memo(function LiveElapsed({
|
||||
});
|
||||
|
||||
interface AssistantMessageProps {
|
||||
occurrenceKey: string;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
workspaceRoot?: string;
|
||||
@@ -783,6 +784,7 @@ const ASSISTANT_IMAGE_MIN_HEIGHT = 160;
|
||||
|
||||
function AssistantMarkdownImage({
|
||||
source,
|
||||
occurrenceKey,
|
||||
alt,
|
||||
hasLeadingContent,
|
||||
client,
|
||||
@@ -790,6 +792,7 @@ function AssistantMarkdownImage({
|
||||
serverId,
|
||||
}: {
|
||||
source: string;
|
||||
occurrenceKey: string;
|
||||
alt?: string;
|
||||
hasLeadingContent: boolean;
|
||||
client?: DaemonClient | null;
|
||||
@@ -803,7 +806,13 @@ function AssistantMarkdownImage({
|
||||
}),
|
||||
[hasLeadingContent],
|
||||
);
|
||||
const image = useAssistantImage({ source, client, workspaceRoot, serverId });
|
||||
const image = useAssistantImage({
|
||||
source,
|
||||
occurrenceKey,
|
||||
client,
|
||||
workspaceRoot,
|
||||
serverId,
|
||||
});
|
||||
const binding = image.status === "failed" ? null : image.binding;
|
||||
const aspectRatio = image.status === "failed" ? null : image.aspectRatio;
|
||||
const imageUri = binding?.uri ?? "";
|
||||
@@ -1410,6 +1419,7 @@ function MarkdownListView({ baseStyle, spacing, children }: MarkdownListViewProp
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(function AssistantMessage({
|
||||
occurrenceKey,
|
||||
message,
|
||||
timestamp: _timestamp,
|
||||
workspaceRoot,
|
||||
@@ -1744,6 +1754,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
<AssistantMarkdownImage
|
||||
key={node.key}
|
||||
source={String(node.attributes?.src ?? "")}
|
||||
occurrenceKey={`${occurrenceKey}:${node.key}`}
|
||||
alt={typeof node.attributes?.alt === "string" ? node.attributes.alt : undefined}
|
||||
hasLeadingContent={hasLeadingContent}
|
||||
client={client}
|
||||
@@ -1753,7 +1764,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [client, fileLinkActions, markdownParser, serverId, workspaceRoot]);
|
||||
}, [client, fileLinkActions, markdownParser, occurrenceKey, serverId, workspaceRoot]);
|
||||
|
||||
const blocks = useMemo(() => splitMarkdownBlocks(message), [message]);
|
||||
const keyedBlocks = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user