重构画布动作上下文与刷新链路

新增编辑器级画布动作上下文与 Provider,统一承载画布动作
将 Agent 对话和任务侧栏的画布刷新迁移到上下文,移除逐层回调透传
统一生成任务完成后的项目刷新入口,避免重复加载项目快照
补齐上下文及刷新链路测试,并更新测试 Provider 装配
This commit is contained in:
2026-07-31 12:00:39 +08:00
parent a36290b2e5
commit ae8d723552
14 changed files with 209 additions and 56 deletions
@@ -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 }) => (
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
{children}
</ImageCanvasActionsProvider>
),
});
}
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(
<EditorAgentConversationPanelView
open
onToggleOpen={vi.fn()}
client={client}
onCanvasRefreshRequested={onCanvasRefreshRequested}
/>,
);
@@ -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();
});
@@ -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?.();
}}
/>
))}
@@ -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 }) => (
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
{children}
</ImageCanvasActionsProvider>
),
});
}
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<typeof createCompletedJobResponse>
>();
const pendingResponse =
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
getExternalGenerationJobStatusMock.mockReturnValueOnce(
pendingResponse.promise,
);
const onJobCompleted = vi.fn();
const { rerender } = render(
const { rerender } = renderWithCanvasActions(
<ToolCallView
toolCall={createToolCall()}
onJobCompleted={onJobCompleted}
@@ -101,21 +114,20 @@ describe('ToolCallView', () => {
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<typeof createCompletedJobResponse>
>();
const jobBResponse = createDeferred<
ReturnType<typeof createCompletedJobResponse>
>();
const jobAResponse =
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
const jobBResponse =
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
getExternalGenerationJobStatusMock.mockImplementation((jobId: string) =>
jobId === 'job-a' ? jobAResponse.promise : jobBResponse.promise,
);
const onJobCompleted = vi.fn();
const { rerender } = render(
const { rerender } = renderWithCanvasActions(
<ToolCallView
toolCall={createToolCall()}
onJobCompleted={onJobCompleted}
@@ -150,5 +162,7 @@ describe('ToolCallView', () => {
expect(await screen.findByText('已完成')).toBeTruthy();
expect(onJobCompleted).toHaveBeenCalledTimes(1);
expect(refreshCanvasMock).toHaveBeenCalledTimes(1);
});
});
@@ -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<string | null>(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;
@@ -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 (
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
{children}
</ImageCanvasActionsProvider>
);
}
function renderHook<Result, Props>(
callback: (initialProps: Props) => Result,
options?: RenderHookOptions<Props>,
): RenderHookResult<Result, Props> {
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 () => {
@@ -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(
@@ -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 }) => (
<ImageCanvasActionsProvider refreshCanvas={refreshCanvas}>
{children}
</ImageCanvasActionsProvider>
),
});
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',
);
});
});
@@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';
export type ImageCanvasActions = {
refreshCanvas: () => void;
};
export const ImageCanvasActionsContext =
createContext<ImageCanvasActions | null>(null);
export function useImageCanvasActions() {
const actions = useContext(ImageCanvasActionsContext);
if (!actions) {
throw new Error(
'useImageCanvasActions must be used within ImageCanvasActionsProvider',
);
}
return actions;
}
@@ -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<ImageCanvasActions>(
() => ({ refreshCanvas }),
[refreshCanvas],
);
return (
<ImageCanvasActionsContext.Provider value={actions}>
{children}
</ImageCanvasActionsContext.Provider>
);
}
@@ -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 }) => (
<ImageCanvasActionsProvider refreshCanvas={vi.fn()}>
{children}
</ImageCanvasActionsProvider>
),
});
}
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-1',
@@ -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 (
<>
<ImageCanvasActionsProvider refreshCanvas={refreshCanvas}>
<ImageCanvasEditorShellView
editorRootRef={editorRootRef}
uploadInputRef={uploadInputRef}
@@ -2539,7 +2531,7 @@ export function ImageCanvasEditorView({
orderId={wechatRechargeOrderConfirmationState.orderId}
/>
) : null}
</>
</ImageCanvasActionsProvider>
);
}
@@ -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}
@@ -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 }) => (
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
{children}
</ImageCanvasActionsProvider>
),
});
}
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();
}
@@ -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<TaskSidebarTab>('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(() => {