task list call back
forward call back
This commit is contained in:
+3
-1
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+4
@@ -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[]>(
|
||||
|
||||
+82
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user