diff --git a/src/components/__tests__/GenerateImageNode.test.tsx b/src/components/__tests__/GenerateImageNode.test.tsx index 40689faa..c02448f8 100644 --- a/src/components/__tests__/GenerateImageNode.test.tsx +++ b/src/components/__tests__/GenerateImageNode.test.tsx @@ -379,6 +379,33 @@ describe("GenerateImageNode", () => { expect(screen.getByText("2 / 3")).toBeInTheDocument(); }); + + it("should switch carousel images using inline history data without fetching", async () => { + render( + + + + ); + + fireEvent.click(screen.getByTitle("Next image")); + + await waitFor(() => { + expect(mockUpdateNodeData).toHaveBeenCalledWith("test-node-1", { + outputImage: "data:image/png;base64,next", + selectedHistoryIndex: 1, + status: "idle", + error: null, + }); + }); + expect(mockFetch).not.toHaveBeenCalledWith("/api/load-generation", expect.anything()); + }); }); describe("Legacy Data Migration", () => { diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 08c4011e..2d0ca18b 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -371,7 +371,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { expect((completeCall![1] as Record).outputImage).toBe("data:image/png;base64,result"); }); + it("should keep generated image data in node carousel history for immediate switching", async () => { + const node = makeNode(); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ success: true, image: "data:image/png;base64,result" }), + }); + + const ctx = makeCtx(node); + await executeNanoBanana(ctx); + + const completeCall = (ctx.updateNodeData as ReturnType).mock.calls.find( + (c: unknown[]) => (c[1] as Record).status === "complete" + ); + expect(completeCall?.[1]).toMatchObject({ + imageHistory: [ + expect.objectContaining({ + image: "data:image/png;base64,result", + }), + ], + }); + }); + + it("should not call server save-generation for browser-local generation paths", async () => { + const node = makeNode(); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ success: true, image: "data:image/png;base64,result" }), + }); + + const ctx = makeCtx(node, { + generationsPath: "browserfs://root/project/generations", + }); + await executeNanoBanana(ctx); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith("/api/generate", expect.anything()); + expect(ctx.trackSaveGeneration).not.toHaveBeenCalled(); + }); + it("should add to global history on success", async () => { const node = makeNode(); mockFetch.mockResolvedValueOnce({ diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 1a05b81f..8ae0b339 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -19,6 +19,7 @@ import { import { pollGenerateTask } from "./pollTaskCompletion"; import { runWithFallback } from "./runWithFallback"; import type { NodeExecutionContext } from "./types"; +import { isBrowserFileSystemPath } from "@/utils/browserFileSystem"; export interface NanoBananaOptions { /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ @@ -196,6 +197,7 @@ export async function executeNanoBanana( // Add to node's carousel history const newHistoryItem = { id: imageId, + image: result.image, timestamp, prompt: finalPrompt, aspectRatio: nodeData.aspectRatio, @@ -235,7 +237,7 @@ export async function executeNanoBanana( } // Auto-save to generations folder if configured - if (generationsPath) { + if (generationsPath && !isBrowserFileSystemPath(generationsPath)) { const savePromise = fetch("/api/save-generation", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/src/types/nodes.ts b/src/types/nodes.ts index d382c634..c08667a6 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -151,10 +151,11 @@ export interface ImageHistoryItem { } /** - * Carousel image item for per-node history (IDs only, images stored externally) + * Carousel image item for per-node history. */ export interface CarouselImageItem { id: string; + image?: string; // Current-session fallback; stripped when externalizing workflow media timestamp: number; prompt: string; aspectRatio: AspectRatio;