task list call back

forward call back
This commit is contained in:
2026-07-14 10:27:26 +08:00
parent 16943476ea
commit d0451f8226
8 changed files with 115 additions and 11 deletions
@@ -122,6 +122,7 @@ function createPendingToolCallMessage(): EditorAgentMessage {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
args: {
object_image_id: 'source-image-1',
reference_image_ids: ['reference-image-1', 'reference-image-2'],
@@ -569,6 +570,7 @@ describe('EditorAgentConversationPanelView', () => {
text: 'internal completed tool output that must stay hidden',
toolCall: {
...pendingMessage.toolCall!,
status: 'completed',
externalJobId: 'task-edit-panel-1',
images: [
{
@@ -609,7 +611,7 @@ describe('EditorAgentConversationPanelView', () => {
text: 'internal cancelled tool output that must stay hidden',
toolCall: {
...pendingMessage.toolCall!,
cancelledAt: '2026-07-03T00:00:11.000Z',
status: 'cancelled',
},
});
@@ -60,6 +60,8 @@ type EditorAgentConversationPanelViewProps = {
layers?: CanvasLayer[];
assets?: EditorAsset[];
onCanvasRefreshRequested?: () => void;
// TODO refactor: move the task list update seperate
onConfirmSent?: () => void;
client?: EditorAgentConversationClient;
};
@@ -237,6 +239,7 @@ export function EditorAgentConversationPanelView({
layers = [],
assets = [],
onCanvasRefreshRequested,
onConfirmSent,
client,
}: EditorAgentConversationPanelViewProps) {
const [hasConversationMounted, setHasConversationMounted] = useState(open);
@@ -270,6 +273,7 @@ export function EditorAgentConversationPanelView({
projectId: effectiveProjectId,
client,
onCanvasRefreshRequested,
onConfirmSent,
});
const [draftText, setDraftText] = useState('');
const [attachments, setAttachments] = useState<EditorAgentAttachmentRef[]>(
@@ -81,6 +81,7 @@ function createClient(): EditorAgentConversationClient {
attachments: [],
toolCall: {
toolName: 'generate_image',
status: 'completed',
externalJobId: 'task-generated-1',
args: {},
displayArgs: {
@@ -168,6 +169,75 @@ describe('useEditorAgentConversation', () => {
);
});
it('replaces a lazily reconciled tool message instead of appending a duplicate', async () => {
const client = createClient();
const pendingMessage: EditorAgentMessage = {
id: 1,
role: 'system',
text: 'pending edit',
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
externalJobId: 'task-edit-1',
args: {
object_image_id: 'source-image',
prompt: '换成像素风',
},
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
error: null,
},
createdAt: '2026-07-03T00:00:00.000Z',
};
vi.mocked(client.getConversation).mockResolvedValue({
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
messages: [pendingMessage],
createdAt: '2026-07-03T00:00:00.000Z',
updatedAt: '2026-07-03T00:00:00.000Z',
});
vi.mocked(client.sendMessage).mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:01.000Z',
},
deltaMessages: [
{
...pendingMessage,
text: '[tool_call:edit-image] output: completed',
toolCall: {
...pendingMessage.toolCall!,
status: 'completed',
images: [{ imageSrc: '/generated/result.png' }],
},
},
],
errorMessage: null,
});
const { result } = renderHook(() =>
useEditorAgentConversation({ projectId: 'project-1', client }),
);
await waitFor(() => {
expect(result.current.messages).toHaveLength(1);
});
await act(async () => {
await result.current.sendMessage('继续');
});
const reconciledMessages = result.current.messages.filter(
(message) => message.id === 1,
);
expect(reconciledMessages).toHaveLength(1);
expect(reconciledMessages[0]?.toolCall?.images[0]?.imageSrc).toBe(
'/generated/result.png',
);
});
it('creates a conversation before sending when the project has no history', async () => {
const client = createClient();
vi.mocked(client.listConversations).mockResolvedValueOnce([]);
@@ -276,6 +346,7 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
args: {
object_image_id: 'source-image',
prompt: '换成像素风',
@@ -296,6 +367,7 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
externalJobId: 'task-edit-1',
args: {
object_image_id: 'source-image',
@@ -317,11 +389,13 @@ describe('useEditorAgentConversation', () => {
createdAt: '2026-07-03T00:00:00.000Z',
});
const onCanvasRefreshRequested = vi.fn();
const onConfirmSent = vi.fn();
const { result } = renderHook(() =>
useEditorAgentConversation({
projectId: 'project-1',
client,
onCanvasRefreshRequested,
onConfirmSent,
}),
);
@@ -353,6 +427,7 @@ describe('useEditorAgentConversation', () => {
'generated/result.png',
);
expect(onCanvasRefreshRequested).toHaveBeenCalledTimes(1);
expect(onConfirmSent).toHaveBeenCalledTimes(1);
});
it('keeps the action busy until confirmation returns an external job id', async () => {
@@ -364,6 +439,7 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
args: { object_image_id: 'source-image', prompt: '换成像素风' },
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
@@ -426,13 +502,14 @@ describe('useEditorAgentConversation', () => {
it('cancels a pending tool call and keeps the replacement in the same position', async () => {
const client = createClient();
const pendingMessage = {
const pendingMessage: EditorAgentMessage = {
id: 0,
role: 'system' as const,
text: 'internal pending tool prompt',
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
args: { object_image_id: 'source-image', prompt: '换成像素风' },
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
@@ -452,8 +529,8 @@ describe('useEditorAgentConversation', () => {
...pendingMessage,
text: 'internal cancelled tool output',
toolCall: {
...pendingMessage.toolCall,
cancelledAt: '2026-07-03T00:00:01.000Z',
...pendingMessage.toolCall!,
status: 'cancelled',
},
});
const onCanvasRefreshRequested = vi.fn();
@@ -466,7 +543,7 @@ describe('useEditorAgentConversation', () => {
);
await waitFor(() => {
expect(result.current.messages[0]?.toolCall?.cancelledAt).toBeUndefined();
expect(result.current.messages[0]?.toolCall?.status).toBe('not_completed');
});
await act(async () => {
await result.current.cancelToolCall(0);
@@ -474,9 +551,7 @@ describe('useEditorAgentConversation', () => {
expect(client.cancelToolCall).toHaveBeenCalledWith('conversation-1', 0);
expect(result.current.messages).toHaveLength(1);
expect(result.current.messages[0]?.toolCall?.cancelledAt).toBe(
'2026-07-03T00:00:01.000Z',
);
expect(result.current.messages[0]?.toolCall?.status).toBe('cancelled');
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
});
@@ -53,6 +53,7 @@ type UseEditorAgentConversationOptions = {
projectId?: string | null;
client?: EditorAgentConversationClient;
onCanvasRefreshRequested?: () => void;
onConfirmSent?: () => void;
};
export type EditorAgentToolCallAction = 'confirm' | 'cancel';
@@ -138,6 +139,7 @@ export function useEditorAgentConversation({
projectId,
client = defaultEditorAgentConversationClient,
onCanvasRefreshRequested,
onConfirmSent,
}: UseEditorAgentConversationOptions) {
const normalizedProjectId = projectId?.trim() ?? '';
const [conversations, setConversations] = useState<
@@ -436,6 +438,9 @@ export function useEditorAgentConversation({
);
if (action === 'confirm') {
requestCanvasRefreshForMessages([updatedMessage]);
if (updatedMessage.toolCall?.externalJobId) {
onConfirmSent?.();
}
}
return updatedMessage;
} catch (error) {
@@ -458,7 +463,7 @@ export function useEditorAgentConversation({
}
}
},
[client, loadConversation, requestCanvasRefreshForMessages],
[client, loadConversation, onConfirmSent, requestCanvasRefreshForMessages],
);
const confirmToolCall = useCallback(
@@ -226,6 +226,7 @@ function createEditorAgentDetailWithGeneration(
attachments: [],
toolCall: {
toolName: 'generate_image',
status: 'completed',
externalJobId: 'task-generation-1',
args: {},
displayArgs: {
@@ -2132,6 +2133,7 @@ describe('ImageCanvasEditorView', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'not_completed',
args: {
object_image_id: 'source-image',
prompt: '把图片换成像素风',
@@ -2154,6 +2156,7 @@ describe('ImageCanvasEditorView', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
status: 'completed',
externalJobId: 'task-edit-canvas-1',
args: {},
displayArgs: createEditImageToolCallDisplayArgs('把图片换成像素风'),
@@ -1249,6 +1249,13 @@ export function ImageCanvasEditorView({
applyProjectSnapshot: applyGeneratedProjectSnapshot,
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
});
const handleEditorAgentConfirmSent = useCallback(() => {
generationSurface.refreshTaskList();
}, [generationSurface]);
const handleEditorAgentJobCompleted = useCallback(() => {
generationSurface.refreshTaskList();
handleEditorAgentCanvasRefreshRequested();
}, [generationSurface, handleEditorAgentCanvasRefreshRequested]);
const effectiveIsAgentConversationOpen =
isAgentConversationEnabled && isAgentConversationOpen;
const toggleAgentConversation = useCallback(() => {
@@ -2126,8 +2133,8 @@ export function ImageCanvasEditorView({
onActivateGenerationDialog: activateCanvasGenerationDialog,
onFocusExternalTask: focusExternalGenerationTask,
onExternalTasksCompleted: handleExternalGenerationTasksCompleted,
onEditorAgentCanvasRefreshRequested:
handleEditorAgentCanvasRefreshRequested,
onEditorAgentCanvasRefreshRequested: handleEditorAgentJobCompleted,
onEditorAgentConfirmSent: handleEditorAgentConfirmSent,
onToggleTaskSidebar: toggleTaskSidebar,
onToggleAgentConversation: toggleAgentConversation,
onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize,
@@ -128,6 +128,7 @@ export type ImageCanvasStageViewProps = {
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
onEditorAgentCanvasRefreshRequested?: () => void;
onEditorAgentConfirmSent?: () => void;
onToggleTaskSidebar: () => void;
onToggleAgentConversation: () => void;
onCropExpandHandlePointerDown: (
@@ -260,6 +261,7 @@ export function ImageCanvasStageView({
onFocusExternalTask,
onExternalTasksCompleted,
onEditorAgentCanvasRefreshRequested,
onEditorAgentConfirmSent,
onToggleTaskSidebar,
onToggleAgentConversation,
onCropExpandHandlePointerDown,
@@ -481,6 +483,7 @@ export function ImageCanvasStageView({
layers={layers}
assets={editorAgentAssets}
onCanvasRefreshRequested={onEditorAgentCanvasRefreshRequested}
onConfirmSent={onEditorAgentConfirmSent}
/>
) : null}
@@ -612,6 +612,9 @@ export function useImageCanvasGenerationWorkflow({
setIsTaskSidebarOpen(true);
setTaskListRefreshKey((key) => key + 1);
}, []);
const refreshTaskList = useCallback(() => {
setTaskListRefreshKey((key) => key + 1);
}, []);
const previousTaskCountRef = useRef(canvasGenerationDialogs.length);
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
@@ -2439,6 +2442,7 @@ export function useImageCanvasGenerationWorkflow({
startCropExpandFrameResize,
removeSelectedLayerBackground,
taskListRefreshKey,
refreshTaskList,
isTaskSidebarOpen,
toggleTaskSidebar: () => setIsTaskSidebarOpen((open) => !open),
extractUiDesignAssets,
@@ -2506,6 +2510,7 @@ export function useImageCanvasGenerationWorkflow({
isMusicMenuOpen,
isTaskSidebarOpen,
taskListRefreshKey,
refreshTaskList,
isPublicationMenuOpen,
isPublicationReferenceMenuOpen,
isSpecMenuOpen,