fix: a2ui actions were not wired up at all
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { A2UISurfaceManager } from './A2UISurfaceManager';
|
import { A2UISurfaceManager } from './A2UISurfaceManager';
|
||||||
import { replaySurfacesFromMessages } from './surfaceAssociation';
|
import { replaySurfacesFromMessages } from './surfaceAssociation';
|
||||||
import type { A2UIResolvedComponent, A2UIServerMessage, A2UIClientAction } from '../../main/a2ui/types';
|
import type { A2UIResolvedComponent, A2UIServerMessage } from '../../main/a2ui/types';
|
||||||
import type { ChatMessage } from '../../main/shared/electronApi';
|
import type { ChatMessage } from '../../main/shared/electronApi';
|
||||||
|
|
||||||
interface UseA2UISurfaceInput {
|
interface UseA2UISurfaceInput {
|
||||||
@@ -31,8 +31,6 @@ interface UseA2UISurfaceResult {
|
|||||||
dismissedSurfaceIds: Set<string>;
|
dismissedSurfaceIds: Set<string>;
|
||||||
/** Dismiss a surface by ID */
|
/** Dismiss a surface by ID */
|
||||||
dismissSurface: (surfaceId: string) => void;
|
dismissSurface: (surfaceId: string) => void;
|
||||||
/** Dispatch an action back to the main process */
|
|
||||||
dispatchAction: (action: A2UIClientAction) => void;
|
|
||||||
/** Update a local data binding (for form inputs) */
|
/** Update a local data binding (for form inputs) */
|
||||||
updateLocalData: (surfaceId: string, path: string, value: unknown) => void;
|
updateLocalData: (surfaceId: string, path: string, value: unknown) => void;
|
||||||
/** Get the data model for a surface */
|
/** Get the data model for a surface */
|
||||||
@@ -148,10 +146,6 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dispatchAction = useCallback((action: A2UIClientAction) => {
|
|
||||||
window.electronAPI?.chat.dispatchA2UIAction?.(action);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateLocalData = useCallback((surfaceId: string, path: string, value: unknown) => {
|
const updateLocalData = useCallback((surfaceId: string, path: string, value: unknown) => {
|
||||||
managerRef.current.updateLocalData(surfaceId, path, value);
|
managerRef.current.updateLocalData(surfaceId, path, value);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -183,7 +177,6 @@ export function useA2UISurface(input: UseA2UISurfaceInput): UseA2UISurfaceResult
|
|||||||
latestSurfaceId,
|
latestSurfaceId,
|
||||||
dismissedSurfaceIds,
|
dismissedSurfaceIds,
|
||||||
dismissSurface,
|
dismissSurface,
|
||||||
dispatchAction,
|
|
||||||
updateLocalData,
|
updateLocalData,
|
||||||
getDataModel,
|
getDataModel,
|
||||||
clearSurfaces,
|
clearSurfaces,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export const AssistantSidebar: React.FC = () => {
|
|||||||
latestSurfaceId,
|
latestSurfaceId,
|
||||||
dismissedSurfaceIds,
|
dismissedSurfaceIds,
|
||||||
dismissSurface,
|
dismissSurface,
|
||||||
dispatchAction,
|
|
||||||
updateLocalData,
|
updateLocalData,
|
||||||
} = useA2UISurface({ conversationId });
|
} = useA2UISurface({ conversationId });
|
||||||
|
|
||||||
@@ -281,7 +280,7 @@ export const AssistantSidebar: React.FC = () => {
|
|||||||
latestSurfaceId={latestSurfaceId}
|
latestSurfaceId={latestSurfaceId}
|
||||||
dismissedSurfaceIds={dismissedSurfaceIds}
|
dismissedSurfaceIds={dismissedSurfaceIds}
|
||||||
onSurfaceDismiss={dismissSurface}
|
onSurfaceDismiss={dismissSurface}
|
||||||
onSurfaceAction={dispatchAction}
|
onSurfaceAction={(a) => handleAssistantAction(a.action, a.payload)}
|
||||||
onSurfaceDataChange={updateLocalData}
|
onSurfaceDataChange={updateLocalData}
|
||||||
currentTurnIndex={currentTurnIndex}
|
currentTurnIndex={currentTurnIndex}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
|||||||
latestSurfaceId,
|
latestSurfaceId,
|
||||||
dismissedSurfaceIds,
|
dismissedSurfaceIds,
|
||||||
dismissSurface,
|
dismissSurface,
|
||||||
dispatchAction,
|
|
||||||
updateLocalData,
|
updateLocalData,
|
||||||
replayFromMessages,
|
replayFromMessages,
|
||||||
} = useA2UISurface({ conversationId });
|
} = useA2UISurface({ conversationId });
|
||||||
@@ -387,7 +386,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
|||||||
latestSurfaceId={latestSurfaceId}
|
latestSurfaceId={latestSurfaceId}
|
||||||
dismissedSurfaceIds={dismissedSurfaceIds}
|
dismissedSurfaceIds={dismissedSurfaceIds}
|
||||||
onSurfaceDismiss={dismissSurface}
|
onSurfaceDismiss={dismissSurface}
|
||||||
onSurfaceAction={dispatchAction}
|
onSurfaceAction={(a) => handleAssistantAction(a.action, a.payload)}
|
||||||
onSurfaceDataChange={updateLocalData}
|
onSurfaceDataChange={updateLocalData}
|
||||||
currentTurnIndex={currentTurnIndex}
|
currentTurnIndex={currentTurnIndex}
|
||||||
/>
|
/>
|
||||||
|
|||||||
188
tests/renderer/a2ui/surfaceActionWiring.test.tsx
Normal file
188
tests/renderer/a2ui/surfaceActionWiring.test.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import type { A2UIClientAction } from '../../../src/main/a2ui/types';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
|
// Capture the onSurfaceAction prop passed to ChatTranscript
|
||||||
|
let capturedOnSurfaceAction: ((action: A2UIClientAction) => void) | undefined;
|
||||||
|
|
||||||
|
vi.mock('../../../src/renderer/components/ChatSurface', () => ({
|
||||||
|
ChatTranscript: (props: { onSurfaceAction?: (action: A2UIClientAction) => void }) => {
|
||||||
|
capturedOnSurfaceAction = props.onSurfaceAction;
|
||||||
|
return React.createElement('div', { 'data-testid': 'mock-chat-transcript' });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../src/renderer/navigation/useChatSurfaceState', () => ({
|
||||||
|
useChatSurfaceState: () => ({
|
||||||
|
messages: [{ role: 'user', content: 'test' }],
|
||||||
|
isStreaming: false,
|
||||||
|
streamingContent: '',
|
||||||
|
toolEvents: [],
|
||||||
|
beginUserTurn: vi.fn(),
|
||||||
|
appendStreamDelta: vi.fn(),
|
||||||
|
recordToolCall: vi.fn(),
|
||||||
|
recordToolResult: vi.fn(),
|
||||||
|
finalizeAssistantTurn: vi.fn(),
|
||||||
|
appendAssistantMessage: vi.fn(),
|
||||||
|
stopStreaming: vi.fn(),
|
||||||
|
abortStreaming: vi.fn(),
|
||||||
|
getStreamingContent: vi.fn(() => ''),
|
||||||
|
setMessages: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../src/renderer/navigation/useChatMessageSender', () => ({
|
||||||
|
useChatMessageSender: () => ({
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../src/renderer/a2ui/useA2UISurface', () => ({
|
||||||
|
useA2UISurface: () => ({
|
||||||
|
surfaces: [],
|
||||||
|
surfacesByTurn: new Map(),
|
||||||
|
latestSurfaceId: null,
|
||||||
|
dismissedSurfaceIds: new Set(),
|
||||||
|
dismissSurface: vi.fn(),
|
||||||
|
updateLocalData: vi.fn(),
|
||||||
|
getDataModel: vi.fn(() => ({})),
|
||||||
|
clearSurfaces: vi.fn(),
|
||||||
|
replayFromMessages: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function setupChatApi() {
|
||||||
|
window.electronAPI.chat = {
|
||||||
|
checkReady: vi.fn(),
|
||||||
|
validateApiKey: vi.fn(),
|
||||||
|
setApiKey: vi.fn(),
|
||||||
|
getApiKey: vi.fn(),
|
||||||
|
getAvailableModels: vi.fn(),
|
||||||
|
setDefaultModel: vi.fn(),
|
||||||
|
getSystemPrompt: vi.fn(),
|
||||||
|
setSystemPrompt: vi.fn(),
|
||||||
|
getConversations: vi.fn(),
|
||||||
|
createConversation: vi.fn(),
|
||||||
|
getConversation: vi.fn().mockResolvedValue({ id: 'conv-1', title: 'Test', model: 'm' }),
|
||||||
|
updateConversation: vi.fn(),
|
||||||
|
deleteConversation: vi.fn(),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
addSystemEvent: vi.fn(),
|
||||||
|
abortMessage: vi.fn(),
|
||||||
|
getHistory: vi.fn().mockResolvedValue([]),
|
||||||
|
clearMessages: vi.fn(),
|
||||||
|
setConversationModel: vi.fn(),
|
||||||
|
analyzeTaxonomy: vi.fn(),
|
||||||
|
analyzeMediaImage: vi.fn(),
|
||||||
|
onStreamDelta: vi.fn(() => vi.fn()),
|
||||||
|
onToolCall: vi.fn(() => vi.fn()),
|
||||||
|
onToolResult: vi.fn(() => vi.fn()),
|
||||||
|
onTitleUpdated: vi.fn(() => vi.fn()),
|
||||||
|
onA2UIMessage: vi.fn(() => vi.fn()),
|
||||||
|
dispatchA2UIAction: vi.fn(),
|
||||||
|
} as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('A2UI surface action wiring', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
capturedOnSurfaceAction = undefined;
|
||||||
|
setupChatApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AssistantSidebar', () => {
|
||||||
|
it('routes surface actions through dispatchAssistantAction, not IPC', async () => {
|
||||||
|
const { AssistantSidebar } = await import(
|
||||||
|
'../../../src/renderer/components/AssistantSidebar/AssistantSidebar'
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSelectedPost = vi.fn();
|
||||||
|
const openTab = vi.fn();
|
||||||
|
const setActiveView = vi.fn();
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
tabs: [],
|
||||||
|
activeTabId: null,
|
||||||
|
posts: [],
|
||||||
|
media: [],
|
||||||
|
setSelectedPost,
|
||||||
|
setSelectedMedia: vi.fn(),
|
||||||
|
openTab,
|
||||||
|
setActiveView,
|
||||||
|
toggleSidebar: vi.fn(),
|
||||||
|
togglePanel: vi.fn(),
|
||||||
|
toggleAssistantSidebar: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(React.createElement(AssistantSidebar));
|
||||||
|
|
||||||
|
expect(capturedOnSurfaceAction).toBeDefined();
|
||||||
|
|
||||||
|
capturedOnSurfaceAction!({
|
||||||
|
surfaceId: 'surface-1',
|
||||||
|
componentId: 'card-1',
|
||||||
|
action: 'openPost',
|
||||||
|
payload: { postId: 'post-abc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setActiveView).toHaveBeenCalledWith('posts');
|
||||||
|
expect(setSelectedPost).toHaveBeenCalledWith('post-abc');
|
||||||
|
expect(openTab).toHaveBeenCalledWith({
|
||||||
|
type: 'post',
|
||||||
|
id: 'post-abc',
|
||||||
|
isTransient: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The IPC no-op handler must NOT be called
|
||||||
|
expect(window.electronAPI.chat.dispatchA2UIAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ChatPanel', () => {
|
||||||
|
it('routes surface actions through dispatchAssistantAction, not IPC', async () => {
|
||||||
|
const { ChatPanel } = await import(
|
||||||
|
'../../../src/renderer/components/ChatPanel/ChatPanel'
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSelectedPost = vi.fn();
|
||||||
|
const openTab = vi.fn();
|
||||||
|
const setActiveView = vi.fn();
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
tabs: [],
|
||||||
|
activeTabId: null,
|
||||||
|
posts: [],
|
||||||
|
media: [],
|
||||||
|
setSelectedPost,
|
||||||
|
setSelectedMedia: vi.fn(),
|
||||||
|
openTab,
|
||||||
|
setActiveView,
|
||||||
|
toggleSidebar: vi.fn(),
|
||||||
|
togglePanel: vi.fn(),
|
||||||
|
toggleAssistantSidebar: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(React.createElement(ChatPanel, { conversationId: 'conv-1' }));
|
||||||
|
|
||||||
|
expect(capturedOnSurfaceAction).toBeDefined();
|
||||||
|
|
||||||
|
capturedOnSurfaceAction!({
|
||||||
|
surfaceId: 'surface-2',
|
||||||
|
componentId: 'card-2',
|
||||||
|
action: 'openPost',
|
||||||
|
payload: { postId: 'post-xyz' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setActiveView).toHaveBeenCalledWith('posts');
|
||||||
|
expect(setSelectedPost).toHaveBeenCalledWith('post-xyz');
|
||||||
|
expect(openTab).toHaveBeenCalledWith({
|
||||||
|
type: 'post',
|
||||||
|
id: 'post-xyz',
|
||||||
|
isTransient: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.electronAPI.chat.dispatchA2UIAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user