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;