wip: still working on agentic UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useAppStore } from '../../store';
|
import { useAppStore } from '../../store';
|
||||||
import { resolveAssistantEditorContext } from '../../navigation/assistantPromptContext';
|
import { resolveAssistantEditorContext } from '../../navigation/assistantPromptContext';
|
||||||
import { planAssistantRequest } from '../../navigation/assistantConversation';
|
import { planAssistantRequest } from '../../navigation/assistantConversation';
|
||||||
@@ -48,6 +48,9 @@ export const AssistantSidebar: React.FC = () => {
|
|||||||
streamingContent,
|
streamingContent,
|
||||||
toolEvents,
|
toolEvents,
|
||||||
beginUserTurn,
|
beginUserTurn,
|
||||||
|
appendStreamDelta,
|
||||||
|
recordToolCall,
|
||||||
|
recordToolResult,
|
||||||
finalizeAssistantTurn,
|
finalizeAssistantTurn,
|
||||||
appendAssistantMessage,
|
appendAssistantMessage,
|
||||||
stopStreaming,
|
stopStreaming,
|
||||||
@@ -81,6 +84,41 @@ export const AssistantSidebar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubDelta = window.electronAPI?.chat.onStreamDelta((data) => {
|
||||||
|
if (data.conversationId === conversationId) {
|
||||||
|
appendStreamDelta(data.delta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubToolCall = window.electronAPI?.chat.onToolCall((data) => {
|
||||||
|
if (data.conversationId === conversationId) {
|
||||||
|
const toolCall = data.toolCall as { name: string; arguments: Record<string, unknown> };
|
||||||
|
recordToolCall(toolCall.name, toolCall.arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubToolResult = window.electronAPI?.chat.onToolResult((data) => {
|
||||||
|
if (data.conversationId === conversationId) {
|
||||||
|
const result = data.result as { name: string; result: unknown };
|
||||||
|
recordToolResult(result.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubTitle = window.electronAPI?.chat.onTitleUpdated((data) => {
|
||||||
|
if (data.conversationId === conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubDelta?.();
|
||||||
|
unsubToolCall?.();
|
||||||
|
unsubToolResult?.();
|
||||||
|
unsubTitle?.();
|
||||||
|
};
|
||||||
|
}, [conversationId, appendStreamDelta, recordToolCall, recordToolResult]);
|
||||||
|
|
||||||
const handleStart = async () => {
|
const handleStart = async () => {
|
||||||
const trimmed = prompt.trim();
|
const trimmed = prompt.trim();
|
||||||
if (!trimmed || isSubmitting) {
|
if (!trimmed || isSubmitting) {
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ export function dispatchAssistantAction(
|
|||||||
return { handled: true };
|
return { handled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.action === 'switchView') {
|
if (input.action === 'switchView' || input.action === 'setActiveView') {
|
||||||
const parsed = switchViewPayloadSchema.safeParse(payload);
|
const parsed = switchViewPayloadSchema.safeParse(payload);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return invalidPayloadError('switchView');
|
return invalidPayloadError(input.action);
|
||||||
}
|
}
|
||||||
const { view } = parsed.data;
|
const { view } = parsed.data;
|
||||||
|
|
||||||
@@ -120,6 +120,11 @@ export function dispatchAssistantAction(
|
|||||||
return { handled: true };
|
return { handled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.action === 'openPanel') {
|
||||||
|
dependencies.togglePanel();
|
||||||
|
return { handled: true };
|
||||||
|
}
|
||||||
|
|
||||||
if (input.action === 'toggleAssistantSidebar') {
|
if (input.action === 'toggleAssistantSidebar') {
|
||||||
dependencies.toggleAssistantSidebar();
|
dependencies.toggleAssistantSidebar();
|
||||||
return { handled: true };
|
return { handled: true };
|
||||||
|
|||||||
51
tests/renderer/components/AssistantSidebar.wiring.test.tsx
Normal file
51
tests/renderer/components/AssistantSidebar.wiring.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
|
||||||
|
|
||||||
|
describe('AssistantSidebar wiring', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const onStreamDelta = vi.fn(() => vi.fn());
|
||||||
|
const onToolCall = vi.fn(() => vi.fn());
|
||||||
|
const onToolResult = vi.fn(() => vi.fn());
|
||||||
|
const onTitleUpdated = vi.fn(() => vi.fn());
|
||||||
|
|
||||||
|
window.electronAPI.chat = {
|
||||||
|
checkReady: vi.fn(),
|
||||||
|
validateApiKey: vi.fn(),
|
||||||
|
setApiKey: vi.fn(),
|
||||||
|
getApiKey: vi.fn(),
|
||||||
|
getProtocolHealth: vi.fn(),
|
||||||
|
getAvailableModels: vi.fn(),
|
||||||
|
setDefaultModel: vi.fn(),
|
||||||
|
getSystemPrompt: vi.fn(),
|
||||||
|
setSystemPrompt: vi.fn(),
|
||||||
|
getConversations: vi.fn(),
|
||||||
|
createConversation: vi.fn(),
|
||||||
|
getConversation: vi.fn(),
|
||||||
|
updateConversation: vi.fn(),
|
||||||
|
deleteConversation: vi.fn(),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
addSystemEvent: vi.fn(),
|
||||||
|
abortMessage: vi.fn(),
|
||||||
|
getHistory: vi.fn(),
|
||||||
|
clearMessages: vi.fn(),
|
||||||
|
setConversationModel: vi.fn(),
|
||||||
|
analyzeTaxonomy: vi.fn(),
|
||||||
|
analyzeMediaImage: vi.fn(),
|
||||||
|
onStreamDelta,
|
||||||
|
onToolCall,
|
||||||
|
onToolResult,
|
||||||
|
onTitleUpdated,
|
||||||
|
} as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes to chat streaming events on mount', () => {
|
||||||
|
render(<AssistantSidebar />);
|
||||||
|
|
||||||
|
expect(window.electronAPI.chat.onStreamDelta).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolCall).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolResult).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onTitleUpdated).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -79,6 +79,51 @@ describe('assistantActionDispatcher', () => {
|
|||||||
expect(setActiveView).toHaveBeenCalledWith('tags');
|
expect(setActiveView).toHaveBeenCalledWith('tags');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports setActiveView action alias for protocol compatibility', () => {
|
||||||
|
const setActiveView = vi.fn();
|
||||||
|
|
||||||
|
const result = dispatchAssistantAction(
|
||||||
|
{
|
||||||
|
action: 'setActiveView',
|
||||||
|
payload: { view: 'chat' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setSelectedPost: vi.fn(),
|
||||||
|
setSelectedMedia: vi.fn(),
|
||||||
|
openTab: vi.fn(),
|
||||||
|
setActiveView,
|
||||||
|
toggleSidebar: vi.fn(),
|
||||||
|
togglePanel: vi.fn(),
|
||||||
|
toggleAssistantSidebar: vi.fn(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.handled).toBe(true);
|
||||||
|
expect(setActiveView).toHaveBeenCalledWith('chat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports openPanel action alias for protocol compatibility', () => {
|
||||||
|
const togglePanel = vi.fn();
|
||||||
|
|
||||||
|
const result = dispatchAssistantAction(
|
||||||
|
{
|
||||||
|
action: 'openPanel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setSelectedPost: vi.fn(),
|
||||||
|
setSelectedMedia: vi.fn(),
|
||||||
|
openTab: vi.fn(),
|
||||||
|
setActiveView: vi.fn(),
|
||||||
|
toggleSidebar: vi.fn(),
|
||||||
|
togglePanel,
|
||||||
|
toggleAssistantSidebar: vi.fn(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.handled).toBe(true);
|
||||||
|
expect(togglePanel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects switchView payload when view is invalid', () => {
|
it('rejects switchView payload when view is invalid', () => {
|
||||||
const setActiveView = vi.fn();
|
const setActiveView = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,103 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import React from 'react';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import path from 'node:path';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
|
||||||
const root = path.resolve(__dirname, '../../..');
|
import { AssistantPanelControls } from '../../../src/renderer/components/AssistantPanelControls';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
async function read(relativePath: string): Promise<string> {
|
|
||||||
return readFile(path.join(root, relativePath), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('assistant sidebar guard rails', () => {
|
describe('assistant sidebar guard rails', () => {
|
||||||
it('keeps assistant sidebar self-contained and avoids opening chat tabs directly', async () => {
|
beforeEach(() => {
|
||||||
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
|
useAppStore.setState({ tabs: [], activeTabId: null, activeView: 'posts' });
|
||||||
|
|
||||||
expect(sidebar).not.toContain('openChatTab(');
|
window.electronAPI.chat = {
|
||||||
expect(sidebar).not.toContain("type: 'chat'");
|
checkReady: vi.fn().mockResolvedValue({ ready: true }),
|
||||||
|
validateApiKey: vi.fn(),
|
||||||
|
setApiKey: vi.fn(),
|
||||||
|
getApiKey: vi.fn(),
|
||||||
|
getProtocolHealth: vi.fn(),
|
||||||
|
getAvailableModels: vi.fn(),
|
||||||
|
setDefaultModel: vi.fn(),
|
||||||
|
getSystemPrompt: vi.fn(),
|
||||||
|
setSystemPrompt: vi.fn(),
|
||||||
|
getConversations: vi.fn(),
|
||||||
|
createConversation: vi.fn(),
|
||||||
|
getConversation: vi.fn(),
|
||||||
|
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()),
|
||||||
|
} as never;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders extended widget branches for assistant panel', async () => {
|
it('keeps assistant sidebar self-contained and avoids opening chat tabs on mount', () => {
|
||||||
const controls = await read('src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx');
|
render(React.createElement(AssistantSidebar));
|
||||||
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
|
|
||||||
|
|
||||||
expect(controls).toContain("element.type === 'chart'");
|
expect(useAppStore.getState().tabs.some((tab) => tab.type === 'chat')).toBe(false);
|
||||||
expect(controls).toContain("element.type === 'form'");
|
|
||||||
expect(controls).toContain("element.type === 'datePicker'");
|
|
||||||
expect(controls).toContain("element.type === 'card'");
|
|
||||||
expect(controls).toContain("element.type === 'image'");
|
|
||||||
expect(controls).toContain("element.type === 'tabs'");
|
|
||||||
expect(controls).toContain("element.type === 'input'");
|
|
||||||
expect(sidebar).toContain('<AssistantPanelControls');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists assistant action feedback events to chat history', async () => {
|
it('renders rich assistant panel widget branches at runtime', () => {
|
||||||
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
|
const onAction = vi.fn();
|
||||||
|
|
||||||
expect(sidebar).toContain('chat.addSystemEvent');
|
const { container } = render(
|
||||||
|
React.createElement(AssistantPanelControls, {
|
||||||
|
elements: [
|
||||||
|
{ type: 'chart', chartType: 'bar', title: 'Trend', series: [{ label: 'Jan', value: 10 }] },
|
||||||
|
{
|
||||||
|
type: 'form',
|
||||||
|
formId: 'f1',
|
||||||
|
submitLabel: 'Submit',
|
||||||
|
action: 'submitNeedsInput',
|
||||||
|
fields: [{ key: 'name', label: 'Name', inputType: 'text', required: true }],
|
||||||
|
},
|
||||||
|
{ type: 'datePicker', key: 'date', label: 'Date', submitLabel: 'Pick', action: 'submitNeedsInput' },
|
||||||
|
{ type: 'card', title: 'Card', body: 'Body', actions: [{ label: 'Open', action: 'openSettings' }] },
|
||||||
|
{ type: 'image', src: 'https://example.com/a.png', caption: 'Image', action: 'openSettings' },
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [{ id: 'tab-1', label: 'Tab 1', elements: [{ type: 'text', text: 'Inside tab' }] }],
|
||||||
|
},
|
||||||
|
{ type: 'input', key: 'query', label: 'Query', inputType: 'text', submitLabel: 'Run', action: 'openSettings' },
|
||||||
|
],
|
||||||
|
onAction,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector('.assistant-panel-chart')).not.toBeNull();
|
||||||
|
expect(container.querySelector('.assistant-panel-form')).not.toBeNull();
|
||||||
|
expect(container.querySelector('.assistant-panel-card')).not.toBeNull();
|
||||||
|
expect(container.querySelector('.assistant-panel-image')).not.toBeNull();
|
||||||
|
expect(container.querySelector('.assistant-panel-tabs')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enforces action confirmation policy before dispatching assistant actions', () => {
|
||||||
|
const onAction = vi.fn();
|
||||||
|
const confirmMock = vi.fn().mockReturnValue(true);
|
||||||
|
Object.defineProperty(window, 'confirm', {
|
||||||
|
value: confirmMock,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
React.createElement(AssistantPanelControls, {
|
||||||
|
elements: [{ type: 'action', label: 'Open Settings', action: 'openSettings' }],
|
||||||
|
actionPolicies: { openSettings: 'confirm' },
|
||||||
|
onAction,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Open Settings'));
|
||||||
|
|
||||||
|
expect(confirmMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onAction).toHaveBeenCalledWith('openSettings', undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,61 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import React from 'react';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import path from 'node:path';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { ChatPanel } from '../../../src/renderer/components/ChatPanel/ChatPanel';
|
||||||
const root = path.resolve(__dirname, '../../..');
|
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
|
||||||
|
|
||||||
async function read(relativePath: string): Promise<string> {
|
|
||||||
return readFile(path.join(root, relativePath), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('chat surface mode usage guards', () => {
|
describe('chat surface mode usage guards', () => {
|
||||||
it('uses shared mode config in both chat surfaces', async () => {
|
beforeEach(() => {
|
||||||
const chatPanel = await read('src/renderer/components/ChatPanel/ChatPanel.tsx');
|
if (!Element.prototype.scrollIntoView) {
|
||||||
const assistantSidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
|
}
|
||||||
|
|
||||||
expect(chatPanel).toContain('getChatSurfaceMode(');
|
window.electronAPI.chat = {
|
||||||
expect(assistantSidebar).toContain('getChatSurfaceMode(');
|
checkReady: vi.fn().mockResolvedValue({ ready: true }),
|
||||||
|
validateApiKey: vi.fn(),
|
||||||
|
setApiKey: vi.fn(),
|
||||||
|
getApiKey: vi.fn(),
|
||||||
|
getProtocolHealth: vi.fn(),
|
||||||
|
getAvailableModels: vi.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
models: [{ id: 'gpt-5', name: 'GPT-5' }],
|
||||||
|
}),
|
||||||
|
setDefaultModel: vi.fn(),
|
||||||
|
getSystemPrompt: vi.fn(),
|
||||||
|
setSystemPrompt: vi.fn(),
|
||||||
|
getConversations: vi.fn(),
|
||||||
|
createConversation: vi.fn(),
|
||||||
|
getConversation: vi.fn().mockResolvedValue({
|
||||||
|
id: 'conv-tab',
|
||||||
|
title: 'Chat',
|
||||||
|
model: 'gpt-5',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
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()),
|
||||||
|
} as never;
|
||||||
|
});
|
||||||
|
|
||||||
expect(chatPanel).toContain('showModelSelector');
|
it('shows model selector in tab chat but not in assistant sidebar', async () => {
|
||||||
expect(chatPanel).toContain('showWelcomeTips');
|
const { container: tabContainer } = render(React.createElement(ChatPanel, { conversationId: 'conv-tab' }));
|
||||||
expect(assistantSidebar).toContain('showWelcomeTips');
|
|
||||||
expect(assistantSidebar).toContain('showToolMarkers');
|
expect(tabContainer.querySelector('.model-selector-button')).not.toBeNull();
|
||||||
|
|
||||||
|
render(React.createElement(AssistantSidebar));
|
||||||
|
|
||||||
|
expect(screen.queryByText('gpt-5')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,79 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import React from 'react';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import path from 'node:path';
|
import { render } from '@testing-library/react';
|
||||||
|
import { ChatPanel } from '../../../src/renderer/components/ChatPanel/ChatPanel';
|
||||||
const root = path.resolve(__dirname, '../../..');
|
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
async function read(relativePath: string): Promise<string> {
|
|
||||||
return readFile(path.join(root, relativePath), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('chat surface shared usage guards', () => {
|
describe('chat surface shared usage guards', () => {
|
||||||
it('uses shared chat surface state hook and transcript renderer in both surfaces', async () => {
|
beforeEach(() => {
|
||||||
const chatPanel = await read('src/renderer/components/ChatPanel/ChatPanel.tsx');
|
if (!Element.prototype.scrollIntoView) {
|
||||||
const assistantSidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
|
}
|
||||||
|
|
||||||
expect(chatPanel).toContain('useChatSurfaceState(');
|
useAppStore.setState({ tabs: [], activeTabId: null, activeView: 'posts' });
|
||||||
expect(chatPanel).toContain('<ChatTranscript');
|
|
||||||
expect(chatPanel).toContain('<AssistantPanelControls');
|
|
||||||
expect(chatPanel).toContain('extractAssistantResponseContent(');
|
|
||||||
|
|
||||||
expect(assistantSidebar).toContain('useChatSurfaceState(');
|
window.electronAPI.chat = {
|
||||||
expect(assistantSidebar).toContain('<ChatTranscript');
|
checkReady: vi.fn().mockResolvedValue({ ready: true }),
|
||||||
expect(assistantSidebar).toContain('<AssistantPanelControls');
|
validateApiKey: vi.fn(),
|
||||||
|
setApiKey: vi.fn(),
|
||||||
|
getApiKey: vi.fn(),
|
||||||
|
getProtocolHealth: vi.fn(),
|
||||||
|
getAvailableModels: vi.fn().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
models: [{ id: 'gpt-5', name: 'GPT-5' }],
|
||||||
|
}),
|
||||||
|
setDefaultModel: vi.fn(),
|
||||||
|
getSystemPrompt: vi.fn(),
|
||||||
|
setSystemPrompt: vi.fn(),
|
||||||
|
getConversations: vi.fn(),
|
||||||
|
createConversation: vi.fn(),
|
||||||
|
getConversation: vi.fn().mockResolvedValue({
|
||||||
|
id: 'conv-tab',
|
||||||
|
title: 'Chat',
|
||||||
|
model: 'gpt-5',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
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()),
|
||||||
|
} as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wires chat panel to load data and subscribe to stream/tool/title events', () => {
|
||||||
|
render(React.createElement(ChatPanel, { conversationId: 'conv-tab' }));
|
||||||
|
|
||||||
|
expect(window.electronAPI.chat.checkReady).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.getConversation).toHaveBeenCalledWith('conv-tab');
|
||||||
|
expect(window.electronAPI.chat.getHistory).toHaveBeenCalledWith('conv-tab');
|
||||||
|
expect(window.electronAPI.chat.getAvailableModels).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(window.electronAPI.chat.onStreamDelta).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolCall).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolResult).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onTitleUpdated).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wires assistant sidebar stream subscriptions and does not auto-open chat tab', () => {
|
||||||
|
render(React.createElement(AssistantSidebar));
|
||||||
|
|
||||||
|
expect(window.electronAPI.chat.onStreamDelta).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolCall).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onToolResult).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.electronAPI.chat.onTitleUpdated).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs.some((tab) => tab.type === 'chat')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user