From ae8d723552b32a94bc6944088a22ddb7e1d9439f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 31 Jul 2026 12:00:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=94=BB=E5=B8=83=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=E4=B8=8A=E4=B8=8B=E6=96=87=E4=B8=8E=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增编辑器级画布动作上下文与 Provider,统一承载画布动作 将 Agent 对话和任务侧栏的画布刷新迁移到上下文,移除逐层回调透传 统一生成任务完成后的项目刷新入口,避免重复加载项目快照 补齐上下文及刷新链路测试,并更新测试 Provider 装配 --- .../EditorAgentConversationPanelView.test.tsx | 20 ++++++-- .../EditorAgentConversationPanelView.tsx | 4 -- .../ToolCallView.test.tsx | 36 +++++++++---- .../EditorAgentConversation/ToolCallView.tsx | 7 +++ .../useEditorAgentConversation.test.tsx | 50 ++++++++++++++----- .../useEditorAgentConversation.ts | 8 +-- .../ImageCanvasActionsContext.test.tsx | 35 +++++++++++++ .../image-editor/ImageCanvasActionsContext.ts | 18 +++++++ .../ImageCanvasActionsProvider.tsx | 25 ++++++++++ .../ImageCanvasEditorShellView.test.tsx | 20 +++++++- .../image-editor/ImageCanvasEditorView.tsx | 16 ++---- .../image-editor/ImageCanvasStageView.tsx | 3 -- .../ImageCanvasTaskSidebarView.test.tsx | 18 ++++++- .../ImageCanvasTaskSidebarView.tsx | 5 +- 14 files changed, 209 insertions(+), 56 deletions(-) create mode 100644 src/components/image-editor/ImageCanvasActionsContext.test.tsx create mode 100644 src/components/image-editor/ImageCanvasActionsContext.ts create mode 100644 src/components/image-editor/ImageCanvasActionsProvider.tsx diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index e64c65f6b..129779663 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -3,11 +3,12 @@ import { act, fireEvent, - render, + render as testingLibraryRender, screen, waitFor, within, } from '@testing-library/react'; +import type { ReactElement, ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { @@ -17,6 +18,7 @@ import type { } from '@/packages/shared/src/contracts'; import type { EditorAgentConversationClient } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts'; import { EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts'; +import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx'; import { useImageCanvasContextStore } from '@/src/components/image-editor/useImageCanvasContextStore.ts'; import { EditorAgentConversationPanelView } from './EditorAgentConversationPanelView.tsx'; @@ -24,6 +26,17 @@ import { EditorAgentConversationPanelView } from './EditorAgentConversationPanel const createEditorProjectResourceMock = vi.hoisted(() => vi.fn()); const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn()); const probeImageFileDimensionsMock = vi.hoisted(() => vi.fn()); +const refreshCanvasMock = vi.fn(); + +function render(ui: ReactElement) { + return testingLibraryRender(ui, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }); +} vi.mock('@/src/services/image-editor/editorProjectClient.ts', async () => { const actual = await vi.importActual< @@ -1801,14 +1814,11 @@ describe('EditorAgentConversationPanelView', () => { resolveConfirmation = resolve; }), ); - const onCanvasRefreshRequested = vi.fn(); - render( , ); @@ -1850,7 +1860,7 @@ describe('EditorAgentConversationPanelView', () => { 'internal completed tool output that must stay hidden', ), ).toBeNull(); - expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); + expect(refreshCanvasMock).not.toHaveBeenCalled(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); }); diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx index 8d52c8c51..6df047e7b 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx @@ -42,7 +42,6 @@ type EditorAgentConversationPanelViewProps = { onToggleOpen: () => void; layers?: CanvasLayer[]; assets?: EditorAsset[]; - onCanvasRefreshRequested?: () => void; // TODO refactor: move the task list update seperate onConfirmSent?: () => void; client?: EditorAgentConversationClient; @@ -57,7 +56,6 @@ export function EditorAgentConversationPanelView({ onToggleOpen, layers = [], assets = [], - onCanvasRefreshRequested, onConfirmSent, client, }: EditorAgentConversationPanelViewProps) { @@ -93,7 +91,6 @@ export function EditorAgentConversationPanelView({ } = useEditorAgentConversation({ projectId: effectiveProjectId, client, - onCanvasRefreshRequested, onConfirmSent, }); const [draftText, setDraftText] = useState(''); @@ -310,7 +307,6 @@ export function EditorAgentConversationPanelView({ onReferenceImage={referenceContextAsset} onJobCompleted={() => { void refreshActiveConversation(); - onCanvasRefreshRequested?.(); }} /> ))} diff --git a/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx b/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx index 8ec8c461f..a9338a306 100644 --- a/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx @@ -1,13 +1,16 @@ /* @vitest-environment jsdom */ import { act, render, screen, waitFor } from '@testing-library/react'; +import type { ReactElement, ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { EditorAgentToolCall } from '@/packages/shared/src/contracts'; +import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx'; import ToolCallView from './ToolCallView.tsx'; const getExternalGenerationJobStatusMock = vi.hoisted(() => vi.fn()); +const refreshCanvasMock = vi.fn(); vi.mock('@/src/services/external-generation', () => ({ getExternalGenerationJobStatus: getExternalGenerationJobStatusMock, @@ -54,9 +57,20 @@ function createCompletedJobResponse(jobId: string) { }; } +function renderWithCanvasActions(ui: ReactElement) { + return render(ui, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }); +} + describe('ToolCallView', () => { beforeEach(() => { getExternalGenerationJobStatusMock.mockReset(); + refreshCanvasMock.mockReset(); }); it.each([ @@ -65,14 +79,13 @@ describe('ToolCallView', () => { ] as const)( 'keeps the server %s state when the pending response arrives late', async (status, statusLabel, error) => { - const pendingResponse = createDeferred< - ReturnType - >(); + const pendingResponse = + createDeferred>(); getExternalGenerationJobStatusMock.mockReturnValueOnce( pendingResponse.promise, ); const onJobCompleted = vi.fn(); - const { rerender } = render( + const { rerender } = renderWithCanvasActions( { expect(screen.getByText(statusLabel)).toBeTruthy(); expect(screen.queryByText('已完成')).toBeNull(); expect(onJobCompleted).not.toHaveBeenCalled(); + expect(refreshCanvasMock).not.toHaveBeenCalled(); }, ); it('ignores job A after switching to job B and completes job B once', async () => { - const jobAResponse = createDeferred< - ReturnType - >(); - const jobBResponse = createDeferred< - ReturnType - >(); + const jobAResponse = + createDeferred>(); + const jobBResponse = + createDeferred>(); getExternalGenerationJobStatusMock.mockImplementation((jobId: string) => jobId === 'job-a' ? jobAResponse.promise : jobBResponse.promise, ); const onJobCompleted = vi.fn(); - const { rerender } = render( + const { rerender } = renderWithCanvasActions( { expect(await screen.findByText('已完成')).toBeTruthy(); expect(onJobCompleted).toHaveBeenCalledTimes(1); + expect(refreshCanvasMock).toHaveBeenCalledTimes(1); }); + }); diff --git a/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx b/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx index 65279fd36..9732d3b8b 100644 --- a/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx +++ b/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import type { EditorAgentToolCall } from '@/packages/shared/src/contracts'; import { editorAgentToolLabel } from '@/src/components/image-editor/EditorAgentConversation/toolCallPresentation.ts'; +import { useImageCanvasActions } from '@/src/components/image-editor/ImageCanvasActionsContext.ts'; import { ResolvedAssetAudio } from '@/src/components/ResolvedAssetAudio.tsx'; import { ResolvedAssetImage } from '@/src/components/ResolvedAssetImage.tsx'; import { ResolvedAssetVideo } from '@/src/components/ResolvedAssetVideo.tsx'; @@ -19,6 +20,7 @@ function ToolCallView({ onJobCompleted?: () => void; onRightClickMenu?: RightClickMenuHandler; }) { + const { refreshCanvas } = useImageCanvasActions(); const videos = toolCall.videos ?? []; const audios = toolCall.audios ?? []; const jobId = toolCall.externalJobId?.trim() || null; @@ -37,9 +39,13 @@ function ToolCallView({ useState(initialDisplayError); const terminalNotifiedRef = useRef(false); const onJobCompletedRef = useRef(onJobCompleted); + const refreshCanvasRef = useRef(refreshCanvas); useEffect(() => { onJobCompletedRef.current = onJobCompleted; }, [onJobCompleted]); + useEffect(() => { + refreshCanvasRef.current = refreshCanvas; + }, [refreshCanvas]); useEffect(() => { setDisplayStatus(initialDisplayStatus); setDisplayError(initialDisplayError); @@ -59,6 +65,7 @@ function ToolCallView({ setDisplayError(response.job.error ?? null); if (!terminalNotifiedRef.current) { terminalNotifiedRef.current = true; + refreshCanvasRef.current(); onJobCompletedRef.current?.(); } return; diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx index 03ff1b3c8..3f1aa8c3e 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.test.tsx @@ -1,6 +1,13 @@ /* @vitest-environment jsdom */ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { + act, + renderHook as testingLibraryRenderHook, + type RenderHookOptions, + type RenderHookResult, + waitFor, +} from '@testing-library/react'; +import type { ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -9,12 +16,37 @@ import { type EditorAgentMessage, type EditorAgentMessageResponse, } from '../../../../packages/shared/src/contracts/editorAgent.ts'; +import { ImageCanvasActionsProvider } from '../ImageCanvasActionsProvider.tsx'; import { EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS, type EditorAgentConversationClient, useEditorAgentConversation, } from './useEditorAgentConversation.ts'; +const refreshCanvasMock = vi.fn(); + +function ImageCanvasActionsTestWrapper({ + children, +}: { + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +function renderHook( + callback: (initialProps: Props) => Result, + options?: RenderHookOptions, +): RenderHookResult { + return testingLibraryRenderHook(callback, { + ...options, + wrapper: ImageCanvasActionsTestWrapper, + }); +} + function createEditImageDisplayArgs(prompt: string) { return { stringArgs: [{ name: 'prompt', label: '修改要求', value: prompt }], @@ -124,12 +156,10 @@ describe('useEditorAgentConversation', () => { it('loads conversations and applies delta messages', async () => { const client = createClient(); - const onCanvasRefreshRequested = vi.fn(); const { result } = renderHook(() => useEditorAgentConversation({ projectId: 'project-1', client, - onCanvasRefreshRequested, }), ); @@ -156,7 +186,7 @@ describe('useEditorAgentConversation', () => { expect(result.current.activeConversation?.title).toBe( '把这个角色改成像素风', ); - expect(onCanvasRefreshRequested).toHaveBeenCalledTimes(1); + expect(refreshCanvasMock).toHaveBeenCalledTimes(1); expect(result.current.messages.map((message) => message.text)).toEqual([ '把这个角色改成像素风', '我来处理', @@ -595,7 +625,6 @@ describe('useEditorAgentConversation', () => { it('does not apply a completed message response after switching conversations', async () => { const client = createClient(); - const onCanvasRefreshRequested = vi.fn(); let resolveSend!: (response: EditorAgentMessageResponse) => void; vi.mocked(client.sendMessage).mockImplementation( () => @@ -607,7 +636,6 @@ describe('useEditorAgentConversation', () => { useEditorAgentConversation({ projectId: 'project-1', client, - onCanvasRefreshRequested, }), ); @@ -682,7 +710,7 @@ describe('useEditorAgentConversation', () => { expect(result.current.activeConversationId).toBe('conversation-2'); expect(result.current.messages).toHaveLength(1); expect(result.current.messages[0]?.text).toBe('第二个会话原有消息'); - expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); + expect(refreshCanvasMock).not.toHaveBeenCalled(); expect( result.current.conversations.find( (conversation) => conversation.conversationId === 'conversation-1', @@ -931,13 +959,11 @@ describe('useEditorAgentConversation', () => { createdAt: '2026-07-03T00:00:00.000Z', }; vi.mocked(client.confirmToolCall).mockResolvedValue(undefined); - const onCanvasRefreshRequested = vi.fn(); const onConfirmSent = vi.fn(); const { result } = renderHook(() => useEditorAgentConversation({ projectId: 'project-1', client, - onCanvasRefreshRequested, onConfirmSent, }), ); @@ -981,7 +1007,7 @@ describe('useEditorAgentConversation', () => { expect(result.current.messages[1]?.toolCall?.externalJobId).toBe( 'task-edit-1', ); - expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); + expect(refreshCanvasMock).not.toHaveBeenCalled(); expect(onConfirmSent).toHaveBeenCalledTimes(1); }); @@ -1109,12 +1135,10 @@ describe('useEditorAgentConversation', () => { createdAt: '2026-07-03T00:00:00.000Z', updatedAt: '2026-07-03T00:00:00.000Z', }); - const onCanvasRefreshRequested = vi.fn(); const { result } = renderHook(() => useEditorAgentConversation({ projectId: 'project-1', client, - onCanvasRefreshRequested, }), ); @@ -1135,7 +1159,7 @@ describe('useEditorAgentConversation', () => { ); expect(result.current.messages).toHaveLength(1); expect(result.current.messages[0]?.toolCall?.status).toBe('cancelled'); - expect(onCanvasRefreshRequested).not.toHaveBeenCalled(); + expect(refreshCanvasMock).not.toHaveBeenCalled(); }); it('rethrows fetch errors and rolls back the optimistic message', async () => { diff --git a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts index e8f610dd5..42787b74d 100644 --- a/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts +++ b/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts @@ -9,6 +9,7 @@ import type { EditorAgentMessageRequest, EditorAgentMessageResponse, } from '@/packages/shared/src/contracts'; +import { useImageCanvasActions } from '@/src/components/image-editor/ImageCanvasActionsContext.ts'; import { cancelEditorAgentToolCall, @@ -47,7 +48,6 @@ export type EditorAgentConversationClient = { type UseEditorAgentConversationOptions = { projectId?: string | null; client?: EditorAgentConversationClient; - onCanvasRefreshRequested?: () => void; onConfirmSent?: () => void; }; @@ -135,9 +135,9 @@ function upsertConversationSummary( export function useEditorAgentConversation({ projectId, client = defaultEditorAgentConversationClient, - onCanvasRefreshRequested, onConfirmSent, }: UseEditorAgentConversationOptions) { + const { refreshCanvas } = useImageCanvasActions(); const normalizedProjectId = projectId?.trim() ?? ''; const [conversations, setConversations] = useState< EditorAgentConversationSummary[] @@ -424,10 +424,10 @@ export function useEditorAgentConversation({ ); }) ) { - onCanvasRefreshRequested?.(); + refreshCanvas(); } }, - [onCanvasRefreshRequested], + [refreshCanvas], ); const applyDeltaMessages = useCallback( diff --git a/src/components/image-editor/ImageCanvasActionsContext.test.tsx b/src/components/image-editor/ImageCanvasActionsContext.test.tsx new file mode 100644 index 000000000..673acf789 --- /dev/null +++ b/src/components/image-editor/ImageCanvasActionsContext.test.tsx @@ -0,0 +1,35 @@ +/* @vitest-environment jsdom */ + +import { act, renderHook } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { + useImageCanvasActions, +} from './ImageCanvasActionsContext'; +import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider'; + +describe('ImageCanvasActionsContext', () => { + it('exposes the editor-scoped canvas actions', () => { + const refreshCanvas = vi.fn(); + const { result } = renderHook(useImageCanvasActions, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }); + + act(() => { + result.current.refreshCanvas(); + }); + + expect(refreshCanvas).toHaveBeenCalledTimes(1); + }); + + it('fails clearly when consumed outside the editor provider', () => { + expect(() => renderHook(useImageCanvasActions)).toThrow( + 'useImageCanvasActions must be used within ImageCanvasActionsProvider', + ); + }); +}); diff --git a/src/components/image-editor/ImageCanvasActionsContext.ts b/src/components/image-editor/ImageCanvasActionsContext.ts new file mode 100644 index 000000000..99d681712 --- /dev/null +++ b/src/components/image-editor/ImageCanvasActionsContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react'; + +export type ImageCanvasActions = { + refreshCanvas: () => void; +}; + +export const ImageCanvasActionsContext = + createContext(null); + +export function useImageCanvasActions() { + const actions = useContext(ImageCanvasActionsContext); + if (!actions) { + throw new Error( + 'useImageCanvasActions must be used within ImageCanvasActionsProvider', + ); + } + return actions; +} diff --git a/src/components/image-editor/ImageCanvasActionsProvider.tsx b/src/components/image-editor/ImageCanvasActionsProvider.tsx new file mode 100644 index 000000000..222d0a69e --- /dev/null +++ b/src/components/image-editor/ImageCanvasActionsProvider.tsx @@ -0,0 +1,25 @@ +import { type ReactNode, useMemo } from 'react'; + +import { + type ImageCanvasActions, + ImageCanvasActionsContext, +} from './ImageCanvasActionsContext'; + +export function ImageCanvasActionsProvider({ + children, + refreshCanvas, +}: { + children: ReactNode; + refreshCanvas: ImageCanvasActions['refreshCanvas']; +}) { + const actions = useMemo( + () => ({ refreshCanvas }), + [refreshCanvas], + ); + + return ( + + {children} + + ); +} diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index 23f387937..bdf6f63fd 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -1,9 +1,15 @@ /* @vitest-environment jsdom */ -import { fireEvent, render, screen, within } from '@testing-library/react'; -import { createRef } from 'react'; +import { + fireEvent, + render as testingLibraryRender, + screen, + within, +} from '@testing-library/react'; +import { createRef, type ReactElement, type ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; +import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider'; import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import type { ImageCanvasMetadataModalViewProps } from './ImageCanvasMetadataModalView'; @@ -11,6 +17,16 @@ import type { ImageCanvasSidebarViewProps } from './ImageCanvasSidebarView'; import type { ImageCanvasStageViewProps } from './ImageCanvasStageView'; import type { ImageCanvasTopbarViewProps } from './ImageCanvasTopbarView'; +function render(ui: ReactElement) { + return testingLibraryRender(ui, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }); +} + function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-1', diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index ce516ccd9..a8791887e 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -32,6 +32,7 @@ import { PlatformRechargePaymentResultDialog, } from '../platform-entry/PlatformRechargePaymentStatusDialogs'; import { usePlatformProfileCenterController } from '../platform-entry/usePlatformProfileCenterController'; +import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider'; import { canvasAssetKindOrNull, DEFAULT_CANVAS_BACKGROUND_COLOR, @@ -1195,7 +1196,7 @@ export function ImageCanvasEditorView({ }, [applyProjectSnapshot, captureCanvasHistory, refreshAssetLibrary], ); - const handleEditorAgentCanvasRefreshRequested = useCallback(() => { + const refreshCanvas = useCallback(() => { if (!projectId) { return; } @@ -1469,10 +1470,6 @@ export function ImageCanvasEditorView({ const handleEditorAgentConfirmSent = useCallback(() => { generationSurface.refreshTaskList(); }, [generationSurface]); - const handleEditorAgentJobCompleted = useCallback(() => { - generationSurface.refreshTaskList(); - handleEditorAgentCanvasRefreshRequested(); - }, [generationSurface, handleEditorAgentCanvasRefreshRequested]); const showGenerationWarning = generationSurface.showGenerationWarning; const handleExternalGenerationTasksCompleted = useCallback( (tasks: ExternalGenerationTaskRecord[]) => { @@ -1486,12 +1483,8 @@ export function ImageCanvasEditorView({ showGenerationWarning(warning); } refreshEditorWalletState(); - void loadEditorProject(projectId) - .then(applyGeneratedProjectSnapshot) - .catch(() => undefined); }, [ - applyGeneratedProjectSnapshot, projectId, refreshEditorWalletState, showGenerationWarning, @@ -2400,7 +2393,6 @@ export function ImageCanvasEditorView({ onActivateGenerationDialog: activateCanvasGenerationDialog, onFocusExternalTask: focusExternalGenerationTask, onExternalTasksCompleted: handleExternalGenerationTasksCompleted, - onEditorAgentCanvasRefreshRequested: handleEditorAgentJobCompleted, onEditorAgentConfirmSent: handleEditorAgentConfirmSent, onToggleTaskSidebar: toggleTaskSidebar, onToggleAgentConversation: toggleAgentConversation, @@ -2465,7 +2457,7 @@ export function ImageCanvasEditorView({ }; return ( - <> + ) : null} - + ); } diff --git a/src/components/image-editor/ImageCanvasStageView.tsx b/src/components/image-editor/ImageCanvasStageView.tsx index fee6cf878..1a9f6ae1b 100644 --- a/src/components/image-editor/ImageCanvasStageView.tsx +++ b/src/components/image-editor/ImageCanvasStageView.tsx @@ -134,7 +134,6 @@ export type ImageCanvasStageViewProps = { onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void; onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void; onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void; - onEditorAgentCanvasRefreshRequested?: () => void; onEditorAgentConfirmSent?: () => void; onToggleTaskSidebar: () => void; onToggleAgentConversation: () => void; @@ -271,7 +270,6 @@ export function ImageCanvasStageView({ onActivateGenerationDialog, onFocusExternalTask, onExternalTasksCompleted, - onEditorAgentCanvasRefreshRequested, onEditorAgentConfirmSent, onToggleTaskSidebar, onToggleAgentConversation, @@ -512,7 +510,6 @@ export function ImageCanvasStageView({ onToggleOpen={onToggleAgentConversation} layers={layers} assets={editorAgentAssets} - onCanvasRefreshRequested={onEditorAgentCanvasRefreshRequested} onConfirmSent={onEditorAgentConfirmSent} /> ) : null} diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx index d13243333..e5479ba81 100644 --- a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx @@ -3,17 +3,31 @@ import { act, fireEvent, - render, + render as testingLibraryRender, screen, waitFor, } from '@testing-library/react'; +import type { ReactElement, ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { listExternalGenerationTasks } from '../../services/external-generation'; +import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider.tsx'; import { ImageCanvasTaskSidebarView } from './ImageCanvasTaskSidebarView'; import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts'; +const refreshCanvasMock = vi.fn(); + +function render(ui: ReactElement) { + return testingLibraryRender(ui, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }); +} + vi.mock('../../services/external-generation', () => ({ listExternalGenerationTasks: vi.fn().mockResolvedValue({ overview: { @@ -30,6 +44,7 @@ const listExternalGenerationTasksMock = vi.mocked(listExternalGenerationTasks); beforeEach(() => { useImageCanvasContextStore.getState().setProjectId('project-1'); + refreshCanvasMock.mockReset(); listExternalGenerationTasksMock.mockClear(); listExternalGenerationTasksMock.mockResolvedValue({ overview: { @@ -448,6 +463,7 @@ describe('ImageCanvasTaskSidebarView', () => { }), ]); expect(onExternalTasksCompleted).toHaveBeenCalledTimes(1); + expect(refreshCanvasMock).toHaveBeenCalledTimes(1); } finally { vi.useRealTimers(); } diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx index 7dcbf6c7a..d987ce431 100644 --- a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx +++ b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx @@ -17,6 +17,7 @@ import { import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { listExternalGenerationTasks } from '../../services/external-generation'; +import { useImageCanvasActions } from './ImageCanvasActionsContext.ts'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import type { CanvasTaskStatus } from './ImageCanvasEditorTypes'; import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts'; @@ -280,6 +281,7 @@ export function ImageCanvasTaskSidebarView({ onFocusExternalTask, onExternalTasksCompleted, }: ImageCanvasTaskSidebarViewProps) { + const { refreshCanvas } = useImageCanvasActions(); const projectId = useImageCanvasContextStore((state) => state.projectId); const normalizedProjectId = projectId?.trim() ?? ''; const [activeTab, setActiveTab] = useState('active'); @@ -334,9 +336,10 @@ export function ImageCanvasTaskSidebarView({ for (const task of newlyCompletedTasks) { completedRefreshNotifiedTaskIdsRef.current.add(task.jobId); } + refreshCanvas(); onExternalTasksCompleted?.(newlyCompletedTasks); }, - [onExternalTasksCompleted], + [onExternalTasksCompleted, refreshCanvas], ); useEffect(() => {